[{"data":1,"prerenderedAt":14083},["ShallowReactive",2],{"post-ip-geolocation-guide":3,"series-source-ip-geolocation-guide":1274},{"id":4,"title":5,"author":6,"body":7,"description":1258,"difficulty":1259,"draft":1260,"extension":1261,"image":1262,"meta":1263,"navigation":666,"path":1264,"pinned":1260,"published":1265,"seo":1266,"series":6,"seriesOrder":6,"stem":1267,"tags":1268,"updated":1272,"__hash__":1273},"posts\u002Fposts\u002Fip-geolocation-guide\u002Findex.md","IP 属地是怎么查出来的？本地查询与在线查询方案对比",null,{"type":8,"value":9,"toc":1234},"minimark",[10,14,22,25,38,49,53,60,74,84,95,102,105,112,117,128,132,143,146,150,156,162,165,169,176,195,198,201,205,248,295,301,305,465,501,510,514,554,559,563,574,577,611,614,618,621,625,845,849,933,937,944,1051,1055,1064,1150,1153,1156,1192,1195,1198,1206,1227,1230],[11,12,13],"h2",{"id":13},"写在前面",[15,16,17,18],"p",{},"很多网站会在「登录记录」「安全中心」里显示你的登录 IP 归属地，比如「福建·福州 · 中国移动」。你可能好奇过：",[19,20,21],"strong",{},"服务器是怎么根据一个 IP 就判断出我在哪个城市、哪个运营商的？",[15,23,24],{},"这篇科普文章讲清楚三件事：",[26,27,28,32,35],"ol",{},[29,30,31],"li",{},"IP 属地到底查的是什么；",[29,33,34],{},"纯本地查询为什么能做到毫秒级且不联网；",[29,36,37],{},"几种主流查询方案的优缺点对比，方便你按场景选型。",[39,40,41],"blockquote",{},[15,42,43,44,48],{},"说明：文中用到的 IP 均为 RFC 5737 保留的文档示例段（TEST-NET-2，",[45,46,47],"code",{},"198.51.100.0\u002F24","），不会对应任何真实地址。",[11,50,52],{"id":51},"先想清楚ip-属地查的是什么","先想清楚：IP 属地查的是什么",[15,54,55,56,59],{},"一个常见误区是「每个 IP 对应一个具体地址」。实际上 IP 属地的本质是 ",[19,57,58],{},"IP 段与归属地的映射","。",[15,61,62,63,73],{},"运营商和机构在申请 IP 时，是按",[19,64,65,66,72],{},"网段（CIDR）",[19,67,68,69,71],{},"成块分配的。比如 ",[45,70,47],{}," 这一个段，一整块都归属同一地区、同一运营商。所以归属地数据天然可以","按段存储","：",[75,76,82],"pre",{"className":77,"code":79,"language":80,"meta":81},[78],"language-text","IP 起始          IP 结束         归属地\n────────────────────────────────────────\n198.51.100.0     198.51.100.255  中国|福建|福州|移动\n198.51.101.0     198.51.101.255  中国|浙江|杭州|电信\n","text","",[45,83,79],{"__ignoreMap":81},[15,85,86,87,90,91,94],{},"全球公网 IP 约有 42 亿个，但",[19,88,89],{},"网段只有几十万条","。正因为存的是「段」而不是「每个 IP」，完整的归属地数据文件才只有几 MB（ip2region 的 ",[45,92,93],{},".xdb"," 约 11MB）。这是「本地离线查询」可行的第一前提。",[15,96,97,98,101],{},"所以，",[19,99,100],{},"IP 属地从来不是精确到街道的定位","，它返回的是段级的「省｜市｜运营商」粗粒度信息。",[11,103,104],{"id":104},"纯本地查询的原理",[15,106,107,108,111],{},"以开源的 ",[19,109,110],{},"ip2region"," 为例，它的查询完全在本地完成，不联网、不请求第三方，几十微秒到毫秒级返回。",[113,114,116],"h3",{"id":115},"第一步把-ip-变成整数","第一步：把 IP 变成整数",[15,118,119,120,123,124,127],{},"以 ",[45,121,122],{},"198.51.100.42"," 为例，它会转成一个 32 位整数 ",[45,125,126],{},"0xC633642A","。IP 的四个数字本质就是 4 个字节，拼起来就是一个整数，这一步没有任何玄学。",[113,129,131],{"id":130},"第二步二分查找","第二步：二分查找",[15,133,134,135,138,139,142],{},"在按起始 IP 排序的段列表里做",[19,136,137],{},"二分查找","，找到满足 ",[45,140,141],{},"段的起始IP ≤ 目标整数 ≤ 段的结束IP"," 的那一段。",[15,144,145],{},"二分查找的复杂度是 O(log N)，N 是几十万条，所以很快——这就是「不靠网络也能毫秒返回」的核心。",[113,147,149],{"id":148},"第三步xdb-文件的两级结构","第三步：.xdb 文件的两级结构",[15,151,152,153,155],{},"ip2region 的 ",[45,154,93],{}," 不是简单顺序文件，内部分两个区，进一步加速：",[75,157,160],{"className":158,"code":159,"language":80,"meta":81},[78],"┌─────────────────────────────┐\n│  索引区 (Index)             │  \u002F\u002F 每段一条定长记录：startIP \u002F endIP \u002F 数据区指针 + 数据长度\n│  …… 二分快速定位段 ……       │\n├─────────────────────────────┤\n│  数据区 (Data)              │  \u002F\u002F 真正存放各段的地点字符串\n│  ……                         │\n└─────────────────────────────┘\n",[45,161,159],{"__ignoreMap":81},[15,163,164],{},"先二分索引区定位到「是哪一段」，再按指针去数据区读出地点字符串，一次磁盘读就能拿到结果。",[113,166,168],{"id":167},"为什么离线数据不会很快过期","为什么「离线数据」不会很快过期",[15,170,171,172,175],{},"IP 归属取决于",[19,173,174],{},"注册机构和运营商的分配","，一个网段通常长期稳定属于某地区、某运营商。所以：",[177,178,179,182,192],"ul",{},[29,180,181],{},"下载一次数据文件，可以用很久；",[29,183,184,185,191],{},"归属偶尔变化时，",[19,186,187,188,190],{},"每月更新一次 ",[45,189,93],{}," 文件","即可（替换文件、不用重启服务，见下文实战）；",[29,193,194],{},"数据源来自 IANA\u002FAPNIC 等机构的分配表与公开归属信息，不是实时探测。",[11,196,197],{"id":197},"主流查询方案对比",[15,199,200],{},"实际开发中，查询 IP 属地主要有三条路线。",[113,202,204],{"id":203},"方案一ip2region纯本地离线推荐","方案一：ip2region（纯本地离线，推荐）",[75,206,210],{"className":207,"code":208,"language":209,"meta":81,"style":81},"language-go shiki shiki-themes github-light github-dark","\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\nregion, err := searcher.SearchByStr(ip)\n\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n","go",[45,211,212,221,242],{"__ignoreMap":81},[213,214,217],"span",{"class":215,"line":216},"line",1,[213,218,220],{"class":219},"sHbNN","\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\n",[213,222,224,228,232,235,239],{"class":215,"line":223},2,[213,225,227],{"class":226},"sIX_F","region, err ",[213,229,231],{"class":230},"siTax",":=",[213,233,234],{"class":226}," searcher.",[213,236,238],{"class":237},"sw2iP","SearchByStr",[213,240,241],{"class":226},"(ip)\n",[213,243,245],{"class":215,"line":244},3,[213,246,247],{"class":219},"\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[249,250,251,264],"table",{},[252,253,254],"thead",{},[255,256,257,261],"tr",{},[258,259,260],"th",{},"优点",[258,262,263],{},"缺点",[265,266,267,276,284],"tbody",{},[255,268,269,273],{},[270,271,272],"td",{},"免费、无网络依赖、无 QPS 限制",[270,274,275],{},"段级粒度，个别段可能显示到相邻地区",[255,277,278,281],{},[270,279,280],{},"毫秒级查询，永不因外部服务挂掉",[270,282,283],{},"数据按月更新，最新分配的段可能短暂缺失",[255,285,286,289],{},[270,287,288],{},"数据文件可随版本更新，不重启",[270,290,291,292],{},"只返回省市区+运营商，无经纬度；",[19,293,294],{},"仅支持 IPv4",[15,296,297,300],{},[19,298,299],{},"适用","：博客\u002F后台的「登录记录显示归属地」这类展示用途。",[113,302,304],{"id":303},"方案二在线-apiip-api-高德-腾讯位置服务","方案二：在线 API（ip-api \u002F 高德 \u002F 腾讯位置服务）",[75,306,308],{"className":207,"code":307,"language":209,"meta":81,"style":81},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\ndefer cancel()\nreq, _ := http.NewRequestWithContext(ctx, \"GET\", \"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\", nil)\nresp, err := http.DefaultClient.Do(req)\nif err != nil {\n    return \"未知\" \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n}\ndefer resp.Body.Close()\n\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[45,309,310,315,347,358,393,410,428,440,446,459],{"__ignoreMap":81},[213,311,312],{"class":215,"line":216},[213,313,314],{"class":219},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\n",[213,316,317,320,322,325,328,331,334,337,341,344],{"class":215,"line":223},[213,318,319],{"class":226},"ctx, cancel ",[213,321,231],{"class":230},[213,323,324],{"class":226}," context.",[213,326,327],{"class":237},"WithTimeout",[213,329,330],{"class":226},"(context.",[213,332,333],{"class":237},"Background",[213,335,336],{"class":226},"(), ",[213,338,340],{"class":339},"suQ91","3",[213,342,343],{"class":230},"*",[213,345,346],{"class":226},"time.Second)\n",[213,348,349,352,355],{"class":215,"line":244},[213,350,351],{"class":230},"defer",[213,353,354],{"class":237}," cancel",[213,356,357],{"class":226},"()\n",[213,359,361,364,366,369,372,375,379,382,385,387,390],{"class":215,"line":360},4,[213,362,363],{"class":226},"req, _ ",[213,365,231],{"class":230},[213,367,368],{"class":226}," http.",[213,370,371],{"class":237},"NewRequestWithContext",[213,373,374],{"class":226},"(ctx, ",[213,376,378],{"class":377},"scXbn","\"GET\"",[213,380,381],{"class":226},", ",[213,383,384],{"class":377},"\"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\"",[213,386,381],{"class":226},[213,388,389],{"class":339},"nil",[213,391,392],{"class":226},")\n",[213,394,396,399,401,404,407],{"class":215,"line":395},5,[213,397,398],{"class":226},"resp, err ",[213,400,231],{"class":230},[213,402,403],{"class":226}," http.DefaultClient.",[213,405,406],{"class":237},"Do",[213,408,409],{"class":226},"(req)\n",[213,411,413,416,419,422,425],{"class":215,"line":412},6,[213,414,415],{"class":230},"if",[213,417,418],{"class":226}," err ",[213,420,421],{"class":230},"!=",[213,423,424],{"class":339}," nil",[213,426,427],{"class":226}," {\n",[213,429,431,434,437],{"class":215,"line":430},7,[213,432,433],{"class":230},"    return",[213,435,436],{"class":377}," \"未知\"",[213,438,439],{"class":219}," \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n",[213,441,443],{"class":215,"line":442},8,[213,444,445],{"class":226},"}\n",[213,447,449,451,454,457],{"class":215,"line":448},9,[213,450,351],{"class":230},[213,452,453],{"class":226}," resp.Body.",[213,455,456],{"class":237},"Close",[213,458,357],{"class":226},[213,460,462],{"class":215,"line":461},10,[213,463,464],{"class":219},"\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[249,466,467,475],{},[252,468,469],{},[255,470,471,473],{},[258,472,260],{},[258,474,263],{},[265,476,477,485,493],{},[255,478,479,482],{},[270,480,481],{},"数据最新、可能更精确",[270,483,484],{},"有频率限制（免费版限 QPS）",[255,486,487,490],{},[270,488,489],{},"部分服务还返回经纬度\u002F时区",[270,491,492],{},"依赖第三方，断网\u002F被墙\u002F限流时拿不到",[255,494,495,498],{},[270,496,497],{},"无需维护数据文件",[270,499,500],{},"每次查询都要发一次 HTTP 请求",[15,502,503,505,506,509],{},[19,504,299],{},"：对精度要求高、且能接受外部依赖与限流的场景。",[19,507,508],{},"务必加超时和降级","，否则第三方一抖，你自己的页面就卡住。",[113,511,513],{"id":512},"方案三maxmind-geolite2离线-mmdb","方案三：MaxMind GeoLite2（离线 .mmdb）",[249,515,516,524],{},[252,517,518],{},[255,519,520,522],{},[258,521,260],{},[258,523,263],{},[265,525,526,534,546],{},[255,527,528,531],{},[270,529,530],{},"全球数据，覆盖广、精度较好",[270,532,533],{},"免费但需注册账号并遵守 EULA；商用需 GeoIP2 商业授权",[255,535,536,543],{},[270,537,538,539,542],{},"标准 ",[45,540,541],{},".mmdb"," 格式，各语言都有库，支持 IPv6",[270,544,545],{},"国内 IP 的省市粒度有时不如 ip2region 细",[255,547,548,551],{},[270,549,550],{},"离线查询",[270,552,553],{},"数据同样要定期更新",[15,555,556,558],{},[19,557,299],{},"：面向全球用户的场景。",[11,560,562],{"id":561},"ipv6本地离线方案的一个盲点","IPv6：本地离线方案的一个盲点",[15,564,565,566,569,570,573],{},"前面聊的都是 IPv4（32 位）。但家宽和移动网络越来越多地",[19,567,568],{},"只下发 IPv6","（128 位）地址，而 ",[19,571,572],{},"ip2region 目前只支持 IPv4","——拿到一个纯 IPv6 地址是查不出归属地的，本地库会返回空结果。",[15,575,576],{},"实际影响与常见应对：",[26,578,579,589,595,605],{},[29,580,581,584,585,588],{},[19,582,583],{},"判断版本","：Go 里 ",[45,586,587],{},"net.ParseIP(ip).To4() != nil"," 就是 IPv4，先判断再查询；",[29,590,591,594],{},[19,592,593],{},"降级展示","：IPv6 直接显示「IPv6 地址」，不显示属地，诚实第一；",[29,596,597,600,601,604],{},[19,598,599],{},"优先取 IPv4","：有反向代理时，从 ",[45,602,603],{},"X-Forwarded-For"," 里挑一个 IPv4 来查；",[29,606,607,610],{},[19,608,609],{},"需要 v6 归属","：换支持 IPv6 的数据源（如 MaxMind GeoLite2）。",[15,612,613],{},"这一点在接入时很容易漏掉，上线前记得拿一个纯 IPv6 地址回归一遍。",[11,615,617],{"id":616},"实战给登录记录加-ip-属地","实战：给登录记录加 IP 属地",[15,619,620],{},"subLog 博客的登录记录就用了 ip2region。完整接入一共四步。",[113,622,624],{"id":623},"_1-进程启动时加载数据文件","1. 进程启动时加载数据文件",[75,626,628],{"className":207,"code":627,"language":209,"meta":81,"style":81},"import (\n    \"sync\u002Fatomic\"\n    \"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb\"\n)\n\n\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\nvar searcher atomic.Pointer[xdb.Searcher]\n\nfunc initSearcher() error {\n    content, err := os.ReadFile(\".\u002Fdata\u002Fip2region.xdb\") \u002F\u002F 约 11MB，一次性读入内存\n    if err != nil {\n        return err\n    }\n    s, err := xdb.NewWithBuffer(content)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s)\n    return nil\n}\n",[45,629,630,638,649,658,662,668,673,704,708,724,749,763,772,778,795,808,815,820,832,840],{"__ignoreMap":81},[213,631,632,635],{"class":215,"line":216},[213,633,634],{"class":230},"import",[213,636,637],{"class":226}," (\n",[213,639,640,643,646],{"class":215,"line":223},[213,641,642],{"class":377},"    \"",[213,644,645],{"class":237},"sync\u002Fatomic",[213,647,648],{"class":377},"\"\n",[213,650,651,653,656],{"class":215,"line":244},[213,652,642],{"class":377},[213,654,655],{"class":237},"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb",[213,657,648],{"class":377},[213,659,660],{"class":215,"line":360},[213,661,392],{"class":226},[213,663,664],{"class":215,"line":395},[213,665,667],{"emptyLinePlaceholder":666},true,"\n",[213,669,670],{"class":215,"line":412},[213,671,672],{"class":219},"\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\n",[213,674,675,678,681,684,687,690,693,696,698,701],{"class":215,"line":430},[213,676,677],{"class":230},"var",[213,679,680],{"class":226}," searcher ",[213,682,683],{"class":237},"atomic",[213,685,686],{"class":226},".",[213,688,689],{"class":237},"Pointer",[213,691,692],{"class":226},"[",[213,694,695],{"class":237},"xdb",[213,697,686],{"class":226},[213,699,700],{"class":237},"Searcher",[213,702,703],{"class":226},"]\n",[213,705,706],{"class":215,"line":442},[213,707,667],{"emptyLinePlaceholder":666},[213,709,710,713,716,719,722],{"class":215,"line":448},[213,711,712],{"class":230},"func",[213,714,715],{"class":237}," initSearcher",[213,717,718],{"class":226},"() ",[213,720,721],{"class":230},"error",[213,723,427],{"class":226},[213,725,726,729,731,734,737,740,743,746],{"class":215,"line":461},[213,727,728],{"class":226},"    content, err ",[213,730,231],{"class":230},[213,732,733],{"class":226}," os.",[213,735,736],{"class":237},"ReadFile",[213,738,739],{"class":226},"(",[213,741,742],{"class":377},"\".\u002Fdata\u002Fip2region.xdb\"",[213,744,745],{"class":226},") ",[213,747,748],{"class":219},"\u002F\u002F 约 11MB，一次性读入内存\n",[213,750,752,755,757,759,761],{"class":215,"line":751},11,[213,753,754],{"class":230},"    if",[213,756,418],{"class":226},[213,758,421],{"class":230},[213,760,424],{"class":339},[213,762,427],{"class":226},[213,764,766,769],{"class":215,"line":765},12,[213,767,768],{"class":230},"        return",[213,770,771],{"class":226}," err\n",[213,773,775],{"class":215,"line":774},13,[213,776,777],{"class":226},"    }\n",[213,779,781,784,786,789,792],{"class":215,"line":780},14,[213,782,783],{"class":226},"    s, err ",[213,785,231],{"class":230},[213,787,788],{"class":226}," xdb.",[213,790,791],{"class":237},"NewWithBuffer",[213,793,794],{"class":226},"(content)\n",[213,796,798,800,802,804,806],{"class":215,"line":797},15,[213,799,754],{"class":230},[213,801,418],{"class":226},[213,803,421],{"class":230},[213,805,424],{"class":339},[213,807,427],{"class":226},[213,809,811,813],{"class":215,"line":810},16,[213,812,768],{"class":230},[213,814,771],{"class":226},[213,816,818],{"class":215,"line":817},17,[213,819,777],{"class":226},[213,821,823,826,829],{"class":215,"line":822},18,[213,824,825],{"class":226},"    searcher.",[213,827,828],{"class":237},"Store",[213,830,831],{"class":226},"(s)\n",[213,833,835,837],{"class":215,"line":834},19,[213,836,433],{"class":230},[213,838,839],{"class":339}," nil\n",[213,841,843],{"class":215,"line":842},20,[213,844,445],{"class":226},[113,846,848],{"id":847},"_2-查询一次","2. 查询一次",[75,850,852],{"className":207,"code":851,"language":209,"meta":81,"style":81},"func ipRegion(ip string) string {\n    region, err := searcher.Load().SearchByStr(ip)\n    if err != nil {\n        return \"未知\"\n    }\n    return region \u002F\u002F \"中国|福建省|福州市|移动\"\n}\n",[45,853,854,877,896,908,915,919,929],{"__ignoreMap":81},[213,855,856,858,861,863,867,870,872,875],{"class":215,"line":216},[213,857,712],{"class":230},[213,859,860],{"class":237}," ipRegion",[213,862,739],{"class":226},[213,864,866],{"class":865},"sPK6S","ip",[213,868,869],{"class":230}," string",[213,871,745],{"class":226},[213,873,874],{"class":230},"string",[213,876,427],{"class":226},[213,878,879,882,884,886,889,892,894],{"class":215,"line":223},[213,880,881],{"class":226},"    region, err ",[213,883,231],{"class":230},[213,885,234],{"class":226},[213,887,888],{"class":237},"Load",[213,890,891],{"class":226},"().",[213,893,238],{"class":237},[213,895,241],{"class":226},[213,897,898,900,902,904,906],{"class":215,"line":244},[213,899,754],{"class":230},[213,901,418],{"class":226},[213,903,421],{"class":230},[213,905,424],{"class":339},[213,907,427],{"class":226},[213,909,910,912],{"class":215,"line":360},[213,911,768],{"class":230},[213,913,914],{"class":377}," \"未知\"\n",[213,916,917],{"class":215,"line":395},[213,918,777],{"class":226},[213,920,921,923,926],{"class":215,"line":412},[213,922,433],{"class":230},[213,924,925],{"class":226}," region ",[213,927,928],{"class":219},"\u002F\u002F \"中国|福建省|福州市|移动\"\n",[213,930,931],{"class":215,"line":430},[213,932,445],{"class":226},[113,934,936],{"id":935},"_3-加一层内存缓存","3. 加一层内存缓存",[15,938,939,940,943],{},"登录记录里同一个 IP 可能反复出现，用 ",[45,941,942],{},"sync.Map"," 按 IP 缓存结果，一次查询终身复用：",[75,945,947],{"className":207,"code":946,"language":209,"meta":81,"style":81},"var regionCache sync.Map\n\nfunc ipRegionCached(ip string) string {\n    if v, ok := regionCache.Load(ip); ok {\n        return v.(string)\n    }\n    region := ipRegion(ip)\n    regionCache.Store(ip, region)\n    return region\n}\n",[45,948,949,964,968,987,1004,1015,1019,1030,1040,1047],{"__ignoreMap":81},[213,950,951,953,956,959,961],{"class":215,"line":216},[213,952,677],{"class":230},[213,954,955],{"class":226}," regionCache ",[213,957,958],{"class":237},"sync",[213,960,686],{"class":226},[213,962,963],{"class":237},"Map\n",[213,965,966],{"class":215,"line":223},[213,967,667],{"emptyLinePlaceholder":666},[213,969,970,972,975,977,979,981,983,985],{"class":215,"line":244},[213,971,712],{"class":230},[213,973,974],{"class":237}," ipRegionCached",[213,976,739],{"class":226},[213,978,866],{"class":865},[213,980,869],{"class":230},[213,982,745],{"class":226},[213,984,874],{"class":230},[213,986,427],{"class":226},[213,988,989,991,994,996,999,1001],{"class":215,"line":360},[213,990,754],{"class":230},[213,992,993],{"class":226}," v, ok ",[213,995,231],{"class":230},[213,997,998],{"class":226}," regionCache.",[213,1000,888],{"class":237},[213,1002,1003],{"class":226},"(ip); ok {\n",[213,1005,1006,1008,1011,1013],{"class":215,"line":395},[213,1007,768],{"class":230},[213,1009,1010],{"class":226}," v.(",[213,1012,874],{"class":230},[213,1014,392],{"class":226},[213,1016,1017],{"class":215,"line":412},[213,1018,777],{"class":226},[213,1020,1021,1024,1026,1028],{"class":215,"line":430},[213,1022,1023],{"class":226},"    region ",[213,1025,231],{"class":230},[213,1027,860],{"class":237},[213,1029,241],{"class":226},[213,1031,1032,1035,1037],{"class":215,"line":442},[213,1033,1034],{"class":226},"    regionCache.",[213,1036,828],{"class":237},[213,1038,1039],{"class":226},"(ip, region)\n",[213,1041,1042,1044],{"class":215,"line":448},[213,1043,433],{"class":230},[213,1045,1046],{"class":226}," region\n",[213,1048,1049],{"class":215,"line":461},[213,1050,445],{"class":226},[113,1052,1054],{"id":1053},"_4-数据热更新不用重启","4. 数据热更新（不用重启）",[15,1056,1057,1058,1060,1061,1063],{},"ip2region 每月发布新数据。下载新文件后重新 ",[45,1059,791],{}," 并 ",[45,1062,828],{}," 覆盖即可，进程完全不用重启：",[75,1065,1067],{"className":207,"code":1066,"language":209,"meta":81,"style":81},"func reloadSearcher(newContent []byte) error {\n    s, err := xdb.NewWithBuffer(newContent)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s) \u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n    return nil\n}\n",[45,1068,1069,1093,1106,1118,1124,1128,1140,1146],{"__ignoreMap":81},[213,1070,1071,1073,1076,1078,1081,1084,1087,1089,1091],{"class":215,"line":216},[213,1072,712],{"class":230},[213,1074,1075],{"class":237}," reloadSearcher",[213,1077,739],{"class":226},[213,1079,1080],{"class":865},"newContent",[213,1082,1083],{"class":226}," []",[213,1085,1086],{"class":230},"byte",[213,1088,745],{"class":226},[213,1090,721],{"class":230},[213,1092,427],{"class":226},[213,1094,1095,1097,1099,1101,1103],{"class":215,"line":223},[213,1096,783],{"class":226},[213,1098,231],{"class":230},[213,1100,788],{"class":226},[213,1102,791],{"class":237},[213,1104,1105],{"class":226},"(newContent)\n",[213,1107,1108,1110,1112,1114,1116],{"class":215,"line":244},[213,1109,754],{"class":230},[213,1111,418],{"class":226},[213,1113,421],{"class":230},[213,1115,424],{"class":339},[213,1117,427],{"class":226},[213,1119,1120,1122],{"class":215,"line":360},[213,1121,768],{"class":230},[213,1123,771],{"class":226},[213,1125,1126],{"class":215,"line":395},[213,1127,777],{"class":226},[213,1129,1130,1132,1134,1137],{"class":215,"line":412},[213,1131,825],{"class":226},[213,1133,828],{"class":237},[213,1135,1136],{"class":226},"(s) ",[213,1138,1139],{"class":219},"\u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n",[213,1141,1142,1144],{"class":215,"line":430},[213,1143,433],{"class":230},[213,1145,839],{"class":339},[213,1147,1148],{"class":215,"line":442},[213,1149,445],{"class":226},[11,1151,1152],{"id":1152},"局限与诚实提醒",[15,1154,1155],{},"无论用哪种方案，都要清楚 IP 属地的边界：",[177,1157,1158,1164,1170,1176,1182],{},[29,1159,1160,1163],{},[19,1161,1162],{},"段级精度","：网段归属是「绝大多数」准确，个别 IP（大型企业自建段、跨省调度段）可能显示到相邻地区或只到省。",[29,1165,1166,1169],{},[19,1167,1168],{},"运营商级，不是定位","：拿到的是「省｜市｜运营商」，不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位，纯 IP 归属做不到。",[29,1171,1172,1175],{},[19,1173,1174],{},"动态 IP","：家宽通常是动态 IP，重拨会变，所以「上次登录在福州，这次在广州」不一定是异常，可能是 IP 变了。",[29,1177,1178,1181],{},[19,1179,1180],{},"别用于安全判定","：用 IP 段做封禁或风控判定风险自负，段级数据不适合当精确依据。",[29,1183,1184,1187,1188,1191],{},[19,1185,1186],{},"隐私与合规","：IP 地址属于个人信息（《个人信息保护法》）。归属地展示建议只出现在",[19,1189,1190],{},"用户本人可见","的页面（登录记录、个人中心）；若在评论区等公开场合展示，建议只显示到省市并明确告知用户。",[11,1193,1194],{"id":1194},"小结",[15,1196,1197],{},"一句话总结：",[39,1199,1200],{},[15,1201,1202,1205],{},[19,1203,1204],{},"IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。"," 它查的是网段归属，不是精确位置。",[177,1207,1208,1214,1221],{},[29,1209,1210,1211,1213],{},"想省心、免费、稳定，用 ",[19,1212,110],{}," 本地离线查询（注意 IPv6 盲点）；",[29,1215,1216,1217,1220],{},"想要最新最细、能接受外部依赖，用",[19,1218,1219],{},"在线 API","（记得超时降级）；",[29,1222,1223,1224,59],{},"面向全球用户、要 IPv6 覆盖，用 ",[19,1225,1226],{},"MaxMind GeoLite2",[15,1228,1229],{},"对个人博客「登录记录显示归属地」这种场景，本地离线方案通常是性价比最高的选择。",[1231,1232,1233],"style",{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":81,"searchDepth":223,"depth":244,"links":1235},[1236,1237,1238,1244,1249,1250,1256,1257],{"id":13,"depth":223,"text":13},{"id":51,"depth":223,"text":52},{"id":104,"depth":223,"text":104,"children":1239},[1240,1241,1242,1243],{"id":115,"depth":244,"text":116},{"id":130,"depth":244,"text":131},{"id":148,"depth":244,"text":149},{"id":167,"depth":244,"text":168},{"id":197,"depth":223,"text":197,"children":1245},[1246,1247,1248],{"id":203,"depth":244,"text":204},{"id":303,"depth":244,"text":304},{"id":512,"depth":244,"text":513},{"id":561,"depth":223,"text":562},{"id":616,"depth":223,"text":617,"children":1251},[1252,1253,1254,1255],{"id":623,"depth":244,"text":624},{"id":847,"depth":244,"text":848},{"id":935,"depth":244,"text":936},{"id":1053,"depth":244,"text":1054},{"id":1152,"depth":223,"text":1152},{"id":1194,"depth":223,"text":1194},"科普 IP 属地查询的原理：IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级，对比 ip2region、在线 API 与 MaxMind 三种方案的优劣，并附登录记录场景的 Go 实战与 IPv6 盲点提醒。","入门",false,"md","\u002Fposts\u002Fip-geolocation-guide\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fip-geolocation-guide","2026-08-07T12:00:00",{"title":5,"description":1258},"posts\u002Fip-geolocation-guide\u002Findex",[1269,1270,1271],"IP","网络","科普","2026-08-16","RY1UPgJaiKsjpC51uqMyG7w_GpCRS13_CX5j1hDD_dM",[1275,2079,2499,2720,3136,4314,4592,6647,7429,8405,9814,10373,11287,12823],{"id":1276,"title":1277,"author":6,"body":1278,"description":2065,"difficulty":2066,"draft":1260,"extension":1261,"image":2067,"meta":2068,"navigation":666,"path":2069,"pinned":1260,"published":2070,"seo":2071,"series":2072,"seriesOrder":216,"stem":2073,"tags":2074,"updated":2070,"__hash__":2078},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":8,"value":1279,"toc":2054},[1280,1283,1286,1289,1513,1516,1558,1561,1579,1582,1731,1734,1870,1873,1876,1934,1937,2051],[11,1281,1282],{"id":1282},"解决问题",[15,1284,1285],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[11,1287,1288],{"id":1288},"查阅资料",[75,1290,1294],{"className":1291,"code":1292,"language":1293,"meta":81,"style":81},"language-bash shiki shiki-themes github-light github-dark","# 启动容器并挂载当前目录到 `\u002Fapp`\ndocker run -it --rm -v \"$PWD\":\u002Fapp -w \u002Fapp ubuntu:20.04 bash\n\n# 在容器内执行以下操作：\napt-get update && apt-get install -y musl-tools golang-go  # 安装 musl 和 Go\nexport CC=musl-gcc  # 指定 musl 编译器\nexport CGO_ENABLED=1  # 启用 CGO\n# 设置代理\ngo env -w GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\ngo build -ldflags '-linkmode external -extldflags \"-static\"' -o myapp  # 静态编译\nexit  # 退出容器\n\n# 验证结果（在宿主机执行）\nfile myapp  # 应显示 \"statically linked\"\nldd myapp   # 应显示 \"not a dynamic executable\"\n\nwget https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n\nENV PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\"\n","bash",[45,1295,1296,1301,1339,1343,1348,1376,1393,1408,1413,1425,1447,1455,1459,1464,1474,1484,1488,1496,1500],{"__ignoreMap":81},[213,1297,1298],{"class":215,"line":216},[213,1299,1300],{"class":219},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[213,1302,1303,1306,1309,1312,1315,1318,1321,1324,1327,1330,1333,1336],{"class":215,"line":223},[213,1304,1305],{"class":237},"docker",[213,1307,1308],{"class":377}," run",[213,1310,1311],{"class":339}," -it",[213,1313,1314],{"class":339}," --rm",[213,1316,1317],{"class":339}," -v",[213,1319,1320],{"class":377}," \"",[213,1322,1323],{"class":226},"$PWD",[213,1325,1326],{"class":377},"\":\u002Fapp",[213,1328,1329],{"class":339}," -w",[213,1331,1332],{"class":377}," \u002Fapp",[213,1334,1335],{"class":377}," ubuntu:20.04",[213,1337,1338],{"class":377}," bash\n",[213,1340,1341],{"class":215,"line":244},[213,1342,667],{"emptyLinePlaceholder":666},[213,1344,1345],{"class":215,"line":360},[213,1346,1347],{"class":219},"# 在容器内执行以下操作：\n",[213,1349,1350,1353,1356,1359,1361,1364,1367,1370,1373],{"class":215,"line":395},[213,1351,1352],{"class":237},"apt-get",[213,1354,1355],{"class":377}," update",[213,1357,1358],{"class":226}," && ",[213,1360,1352],{"class":237},[213,1362,1363],{"class":377}," install",[213,1365,1366],{"class":339}," -y",[213,1368,1369],{"class":377}," musl-tools",[213,1371,1372],{"class":377}," golang-go",[213,1374,1375],{"class":219},"  # 安装 musl 和 Go\n",[213,1377,1378,1381,1384,1387,1390],{"class":215,"line":412},[213,1379,1380],{"class":230},"export",[213,1382,1383],{"class":226}," CC",[213,1385,1386],{"class":230},"=",[213,1388,1389],{"class":226},"musl-gcc  ",[213,1391,1392],{"class":219},"# 指定 musl 编译器\n",[213,1394,1395,1397,1400,1402,1405],{"class":215,"line":430},[213,1396,1380],{"class":230},[213,1398,1399],{"class":226}," CGO_ENABLED",[213,1401,1386],{"class":230},[213,1403,1404],{"class":339},"1",[213,1406,1407],{"class":219},"  # 启用 CGO\n",[213,1409,1410],{"class":215,"line":442},[213,1411,1412],{"class":219},"# 设置代理\n",[213,1414,1415,1417,1420,1422],{"class":215,"line":448},[213,1416,209],{"class":237},[213,1418,1419],{"class":377}," env",[213,1421,1329],{"class":339},[213,1423,1424],{"class":377}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[213,1426,1427,1429,1432,1435,1438,1441,1444],{"class":215,"line":461},[213,1428,209],{"class":237},[213,1430,1431],{"class":377}," build",[213,1433,1434],{"class":339}," -ldflags",[213,1436,1437],{"class":377}," '-linkmode external -extldflags \"-static\"'",[213,1439,1440],{"class":339}," -o",[213,1442,1443],{"class":377}," myapp",[213,1445,1446],{"class":219},"  # 静态编译\n",[213,1448,1449,1452],{"class":215,"line":751},[213,1450,1451],{"class":339},"exit",[213,1453,1454],{"class":219},"  # 退出容器\n",[213,1456,1457],{"class":215,"line":765},[213,1458,667],{"emptyLinePlaceholder":666},[213,1460,1461],{"class":215,"line":774},[213,1462,1463],{"class":219},"# 验证结果（在宿主机执行）\n",[213,1465,1466,1469,1471],{"class":215,"line":780},[213,1467,1468],{"class":237},"file",[213,1470,1443],{"class":377},[213,1472,1473],{"class":219},"  # 应显示 \"statically linked\"\n",[213,1475,1476,1479,1481],{"class":215,"line":797},[213,1477,1478],{"class":237},"ldd",[213,1480,1443],{"class":377},[213,1482,1483],{"class":219},"   # 应显示 \"not a dynamic executable\"\n",[213,1485,1486],{"class":215,"line":810},[213,1487,667],{"emptyLinePlaceholder":666},[213,1489,1490,1493],{"class":215,"line":817},[213,1491,1492],{"class":237},"wget",[213,1494,1495],{"class":377}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[213,1497,1498],{"class":215,"line":822},[213,1499,667],{"emptyLinePlaceholder":666},[213,1501,1502,1505,1508,1511],{"class":215,"line":834},[213,1503,1504],{"class":237},"ENV",[213,1506,1507],{"class":377}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[213,1509,1510],{"class":226},"$PATH",[213,1512,648],{"class":377},[113,1514,1515],{"id":1515},"遇到问题",[26,1517,1518,1544],{},[29,1519,1520,1523,1524,1527,1530,1531,1534,1536,1537,1539],{},[45,1521,1522],{},"go mod tidy"," 时报错 ",[45,1525,1526],{},"tls: failed to verify certificate: x509: certificate",[1528,1529],"br",{},"解决：",[45,1532,1533],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[1528,1535],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[1528,1538],{},[1540,1541],"img",{"alt":1542,"src":1543},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[29,1545,1546,1547,1550,1551,1553,1554,1557],{},"直接安装 ",[45,1548,1549],{},"golang-go"," 其版本过低",[1528,1552],{},"解决: ",[45,1555,1556],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[11,1559,1560],{"id":1560},"最终方案",[26,1562,1563,1570,1573],{},[29,1564,1565,1566,1569],{},"下载 ",[45,1567,1568],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[29,1571,1572],{},"编写 Dockerfile",[29,1574,1575,1576],{},"执行 ",[45,1577,1578],{},"docker build -t go-build:v1 .",[15,1580,1581],{},"Dockerfile 内容：",[75,1583,1587],{"className":1584,"code":1585,"language":1586,"meta":81,"style":81},"language-dockerfile shiki shiki-themes github-light github-dark","FROM ubuntu:20.04\n\nLABEL org.opencontainers.image.authors=\"sutong\"\n\nCOPY go\u002F \u002Fgo\u002F\n\nENV PATH=\"\u002Fgo\u002Fbin:$PATH\"\n\nRUN apt-get update && apt-get install -y musl-tools\n\n## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\nRUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n# 指定 musl 编译器\nENV CC=musl-gcc\n# 启用 CGO\nENV CGO_ENABLED=1\n\nVOLUME \u002Fdata\n\nWORKDIR \u002Fdata\n\nCMD [\"\u002Fbin\u002Fbash\"]\n","dockerfile",[45,1588,1589,1597,1601,1612,1616,1624,1628,1638,1642,1650,1654,1659,1666,1670,1677,1682,1689,1693,1701,1705,1712,1717],{"__ignoreMap":81},[213,1590,1591,1594],{"class":215,"line":216},[213,1592,1593],{"class":230},"FROM",[213,1595,1596],{"class":226}," ubuntu:20.04\n",[213,1598,1599],{"class":215,"line":223},[213,1600,667],{"emptyLinePlaceholder":666},[213,1602,1603,1606,1609],{"class":215,"line":244},[213,1604,1605],{"class":230},"LABEL",[213,1607,1608],{"class":226}," org.opencontainers.image.authors=",[213,1610,1611],{"class":377},"\"sutong\"\n",[213,1613,1614],{"class":215,"line":360},[213,1615,667],{"emptyLinePlaceholder":666},[213,1617,1618,1621],{"class":215,"line":395},[213,1619,1620],{"class":230},"COPY",[213,1622,1623],{"class":226}," go\u002F \u002Fgo\u002F\n",[213,1625,1626],{"class":215,"line":412},[213,1627,667],{"emptyLinePlaceholder":666},[213,1629,1630,1632,1635],{"class":215,"line":430},[213,1631,1504],{"class":230},[213,1633,1634],{"class":226}," PATH=",[213,1636,1637],{"class":377},"\"\u002Fgo\u002Fbin:$PATH\"\n",[213,1639,1640],{"class":215,"line":442},[213,1641,667],{"emptyLinePlaceholder":666},[213,1643,1644,1647],{"class":215,"line":448},[213,1645,1646],{"class":230},"RUN",[213,1648,1649],{"class":226}," apt-get update && apt-get install -y musl-tools\n",[213,1651,1652],{"class":215,"line":461},[213,1653,667],{"emptyLinePlaceholder":666},[213,1655,1656],{"class":215,"line":751},[213,1657,1658],{"class":219},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[213,1660,1661,1663],{"class":215,"line":765},[213,1662,1646],{"class":230},[213,1664,1665],{"class":226}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[213,1667,1668],{"class":215,"line":774},[213,1669,1392],{"class":219},[213,1671,1672,1674],{"class":215,"line":780},[213,1673,1504],{"class":230},[213,1675,1676],{"class":226}," CC=musl-gcc\n",[213,1678,1679],{"class":215,"line":797},[213,1680,1681],{"class":219},"# 启用 CGO\n",[213,1683,1684,1686],{"class":215,"line":810},[213,1685,1504],{"class":230},[213,1687,1688],{"class":226}," CGO_ENABLED=1\n",[213,1690,1691],{"class":215,"line":817},[213,1692,667],{"emptyLinePlaceholder":666},[213,1694,1695,1698],{"class":215,"line":822},[213,1696,1697],{"class":230},"VOLUME",[213,1699,1700],{"class":226}," \u002Fdata\n",[213,1702,1703],{"class":215,"line":834},[213,1704,667],{"emptyLinePlaceholder":666},[213,1706,1707,1710],{"class":215,"line":842},[213,1708,1709],{"class":230},"WORKDIR",[213,1711,1700],{"class":226},[213,1713,1715],{"class":215,"line":1714},21,[213,1716,667],{"emptyLinePlaceholder":666},[213,1718,1720,1723,1726,1729],{"class":215,"line":1719},22,[213,1721,1722],{"class":230},"CMD",[213,1724,1725],{"class":226}," [",[213,1727,1728],{"class":377},"\"\u002Fbin\u002Fbash\"",[213,1730,703],{"class":226},[113,1732,1733],{"id":1733},"构建",[75,1735,1737],{"className":1291,"code":1736,"language":1293,"meta":81,"style":81},"# docker build -t go-build:v1 .\n\n# 直接执行编译 编译后删除\ndocker run --rm -v .:\u002Fdata go-build:v1 go build -o app_musl\n\n# 构建编译环境\ndocker run -it -d -v .:\u002Fdata --name \u003C容器名称> go-build:v1 \u002Fbin\u002Fbash\n# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n\ndocker exec -it \u003C容器名称> go build -o \u003Cmyapp>\n# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[45,1738,1739,1744,1748,1753,1779,1783,1788,1823,1828,1832,1865],{"__ignoreMap":81},[213,1740,1741],{"class":215,"line":216},[213,1742,1743],{"class":219},"# docker build -t go-build:v1 .\n",[213,1745,1746],{"class":215,"line":223},[213,1747,667],{"emptyLinePlaceholder":666},[213,1749,1750],{"class":215,"line":244},[213,1751,1752],{"class":219},"# 直接执行编译 编译后删除\n",[213,1754,1755,1757,1759,1761,1763,1766,1769,1772,1774,1776],{"class":215,"line":360},[213,1756,1305],{"class":237},[213,1758,1308],{"class":377},[213,1760,1314],{"class":339},[213,1762,1317],{"class":339},[213,1764,1765],{"class":377}," .:\u002Fdata",[213,1767,1768],{"class":377}," go-build:v1",[213,1770,1771],{"class":377}," go",[213,1773,1431],{"class":377},[213,1775,1440],{"class":339},[213,1777,1778],{"class":377}," app_musl\n",[213,1780,1781],{"class":215,"line":395},[213,1782,667],{"emptyLinePlaceholder":666},[213,1784,1785],{"class":215,"line":412},[213,1786,1787],{"class":219},"# 构建编译环境\n",[213,1789,1790,1792,1794,1796,1799,1801,1803,1806,1809,1812,1815,1818,1820],{"class":215,"line":430},[213,1791,1305],{"class":237},[213,1793,1308],{"class":377},[213,1795,1311],{"class":339},[213,1797,1798],{"class":339}," -d",[213,1800,1317],{"class":339},[213,1802,1765],{"class":377},[213,1804,1805],{"class":339}," --name",[213,1807,1808],{"class":230}," \u003C",[213,1810,1811],{"class":377},"容器名",[213,1813,1814],{"class":226},"称",[213,1816,1817],{"class":230},">",[213,1819,1768],{"class":377},[213,1821,1822],{"class":377}," \u002Fbin\u002Fbash\n",[213,1824,1825],{"class":215,"line":442},[213,1826,1827],{"class":219},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[213,1829,1830],{"class":215,"line":448},[213,1831,667],{"emptyLinePlaceholder":666},[213,1833,1834,1836,1839,1841,1843,1845,1847,1849,1851,1853,1855,1857,1860,1862],{"class":215,"line":461},[213,1835,1305],{"class":237},[213,1837,1838],{"class":377}," exec",[213,1840,1311],{"class":339},[213,1842,1808],{"class":230},[213,1844,1811],{"class":377},[213,1846,1814],{"class":226},[213,1848,1817],{"class":230},[213,1850,1771],{"class":377},[213,1852,1431],{"class":377},[213,1854,1440],{"class":339},[213,1856,1808],{"class":230},[213,1858,1859],{"class":377},"myap",[213,1861,15],{"class":226},[213,1863,1864],{"class":230},">\n",[213,1866,1867],{"class":215,"line":751},[213,1868,1869],{"class":219},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[113,1871,1872],{"id":1872},"额外注意",[15,1874,1875],{},"若没科学上网需要设置代理：",[75,1877,1879],{"className":1291,"code":1878,"language":1293,"meta":81,"style":81},"docker exec -it \u003C容器名称> \u002Fbin\u002Fbash\ngo env -w GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\ngo build -ldflags '-linkmode external -extldflags \"-static\"' -o myapp  # 静态编译\ngo build # musl 库编译\n",[45,1880,1881,1899,1909,1925],{"__ignoreMap":81},[213,1882,1883,1885,1887,1889,1891,1893,1895,1897],{"class":215,"line":216},[213,1884,1305],{"class":237},[213,1886,1838],{"class":377},[213,1888,1311],{"class":339},[213,1890,1808],{"class":230},[213,1892,1811],{"class":377},[213,1894,1814],{"class":226},[213,1896,1817],{"class":230},[213,1898,1822],{"class":377},[213,1900,1901,1903,1905,1907],{"class":215,"line":223},[213,1902,209],{"class":237},[213,1904,1419],{"class":377},[213,1906,1329],{"class":339},[213,1908,1424],{"class":377},[213,1910,1911,1913,1915,1917,1919,1921,1923],{"class":215,"line":244},[213,1912,209],{"class":237},[213,1914,1431],{"class":377},[213,1916,1434],{"class":339},[213,1918,1437],{"class":377},[213,1920,1440],{"class":339},[213,1922,1443],{"class":377},[213,1924,1446],{"class":219},[213,1926,1927,1929,1931],{"class":215,"line":360},[213,1928,209],{"class":237},[213,1930,1431],{"class":377},[213,1932,1933],{"class":219}," # musl 库编译\n",[113,1935,1936],{"id":1936},"运行环境示例",[75,1938,1940],{"className":1584,"code":1939,"language":1586,"meta":81,"style":81},"FROM alpine:latest\n\nLABEL org.opencontainers.image.authors=\"sutong\"\n\nCOPY glog_musl \u002Fapp\u002F\n\nCOPY conf.ini \u002Fdata\u002F\nCOPY ipname.ini \u002Fdata\u002F\n\nWORKDIR \u002Fapp\n\nRUN chmod 777 \u002Fapp\u002Fglog_musl\n\nCMD [\".\u002Fglog_musl\", \"logsweb\", \"\u002Fdata\u002Fconf.ini\"]\n\n# docker build -t glog:v1 .\n# docker run -p 6801:6801 glog:v1\n",[45,1941,1942,1949,1953,1961,1965,1972,1976,1983,1990,1994,2001,2005,2012,2016,2037,2041,2046],{"__ignoreMap":81},[213,1943,1944,1946],{"class":215,"line":216},[213,1945,1593],{"class":230},[213,1947,1948],{"class":226}," alpine:latest\n",[213,1950,1951],{"class":215,"line":223},[213,1952,667],{"emptyLinePlaceholder":666},[213,1954,1955,1957,1959],{"class":215,"line":244},[213,1956,1605],{"class":230},[213,1958,1608],{"class":226},[213,1960,1611],{"class":377},[213,1962,1963],{"class":215,"line":360},[213,1964,667],{"emptyLinePlaceholder":666},[213,1966,1967,1969],{"class":215,"line":395},[213,1968,1620],{"class":230},[213,1970,1971],{"class":226}," glog_musl \u002Fapp\u002F\n",[213,1973,1974],{"class":215,"line":412},[213,1975,667],{"emptyLinePlaceholder":666},[213,1977,1978,1980],{"class":215,"line":430},[213,1979,1620],{"class":230},[213,1981,1982],{"class":226}," conf.ini \u002Fdata\u002F\n",[213,1984,1985,1987],{"class":215,"line":442},[213,1986,1620],{"class":230},[213,1988,1989],{"class":226}," ipname.ini \u002Fdata\u002F\n",[213,1991,1992],{"class":215,"line":448},[213,1993,667],{"emptyLinePlaceholder":666},[213,1995,1996,1998],{"class":215,"line":461},[213,1997,1709],{"class":230},[213,1999,2000],{"class":226}," \u002Fapp\n",[213,2002,2003],{"class":215,"line":751},[213,2004,667],{"emptyLinePlaceholder":666},[213,2006,2007,2009],{"class":215,"line":765},[213,2008,1646],{"class":230},[213,2010,2011],{"class":226}," chmod 777 \u002Fapp\u002Fglog_musl\n",[213,2013,2014],{"class":215,"line":774},[213,2015,667],{"emptyLinePlaceholder":666},[213,2017,2018,2020,2022,2025,2027,2030,2032,2035],{"class":215,"line":780},[213,2019,1722],{"class":230},[213,2021,1725],{"class":226},[213,2023,2024],{"class":377},"\".\u002Fglog_musl\"",[213,2026,381],{"class":226},[213,2028,2029],{"class":377},"\"logsweb\"",[213,2031,381],{"class":226},[213,2033,2034],{"class":377},"\"\u002Fdata\u002Fconf.ini\"",[213,2036,703],{"class":226},[213,2038,2039],{"class":215,"line":797},[213,2040,667],{"emptyLinePlaceholder":666},[213,2042,2043],{"class":215,"line":810},[213,2044,2045],{"class":219},"# docker build -t glog:v1 .\n",[213,2047,2048],{"class":215,"line":817},[213,2049,2050],{"class":219},"# docker run -p 6801:6801 glog:v1\n",[1231,2052,2053],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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":81,"searchDepth":223,"depth":244,"links":2055},[2056,2057,2060],{"id":1282,"depth":223,"text":1282},{"id":1288,"depth":223,"text":1288,"children":2058},[2059],{"id":1515,"depth":244,"text":1515},{"id":1560,"depth":223,"text":1560,"children":2061},[2062,2063,2064],{"id":1733,"depth":244,"text":1733},{"id":1872,"depth":244,"text":1872},{"id":1936,"depth":244,"text":1936},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","进阶","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":1277,"description":2065},"Docker 实践笔记","posts\u002Fdocker-musl-build\u002Findex",[2075,2076,2077],"Docker","Go","运维","YLtdOOc-gMVDSNjE-ntKXz-7hff-VtvMB2dB5RXNi1o",{"id":2080,"title":2081,"author":6,"body":2082,"description":2489,"difficulty":1259,"draft":1260,"extension":1261,"image":2490,"meta":2491,"navigation":666,"path":2492,"pinned":1260,"published":2493,"seo":2494,"series":2072,"seriesOrder":223,"stem":2495,"tags":2496,"updated":2493,"__hash__":2498},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":8,"value":2083,"toc":2479},[2084,2087,2090,2093,2097,2100,2117,2121,2154,2158,2165,2215,2218,2437,2440,2443,2473,2476],[11,2085,2086],{"id":2086},"前言",[15,2088,2089],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[11,2091,2092],{"id":2092},"解决方案",[113,2094,2096],{"id":2095},"_1-安装用户管理工具","1. 安装用户管理工具",[15,2098,2099],{},"若镜像非自带用户管理：",[75,2101,2103],{"className":1584,"code":2102,"language":1586,"meta":81,"style":81},"# 权限管理\nRUN apk add --no-cache shadow\n",[45,2104,2105,2110],{"__ignoreMap":81},[213,2106,2107],{"class":215,"line":216},[213,2108,2109],{"class":219},"# 权限管理\n",[213,2111,2112,2114],{"class":215,"line":223},[213,2113,1646],{"class":230},[213,2115,2116],{"class":226}," apk add --no-cache shadow\n",[113,2118,2120],{"id":2119},"_2-创建用户并修改权限","2. 创建用户并修改权限",[75,2122,2124],{"className":1584,"code":2123,"language":1586,"meta":81,"style":81},"# 创建用户组和用户\nRUN addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n\n# 修改目录权限\nRUN chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[45,2125,2126,2131,2138,2142,2147],{"__ignoreMap":81},[213,2127,2128],{"class":215,"line":216},[213,2129,2130],{"class":219},"# 创建用户组和用户\n",[213,2132,2133,2135],{"class":215,"line":223},[213,2134,1646],{"class":230},[213,2136,2137],{"class":226}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[213,2139,2140],{"class":215,"line":244},[213,2141,667],{"emptyLinePlaceholder":666},[213,2143,2144],{"class":215,"line":360},[213,2145,2146],{"class":219},"# 修改目录权限\n",[213,2148,2149,2151],{"class":215,"line":395},[213,2150,1646],{"class":230},[213,2152,2153],{"class":226}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[113,2155,2157],{"id":2156},"_3-指定用户启动","3. 指定用户启动",[15,2159,2160,2161,2164],{},"执行 docker 时加 ",[45,2162,2163],{},"--user"," 参数：",[75,2166,2168],{"className":1291,"code":2167,"language":1293,"meta":81,"style":81},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[45,2169,2170],{"__ignoreMap":81},[213,2171,2172,2174,2176,2178,2181,2183,2186,2189,2191,2193,2196,2199,2201,2204,2207,2210,2213],{"class":215,"line":216},[213,2173,1305],{"class":237},[213,2175,1308],{"class":377},[213,2177,1311],{"class":339},[213,2179,2180],{"class":339}," --user",[213,2182,1808],{"class":230},[213,2184,2185],{"class":377},"app_use",[213,2187,2188],{"class":226},"r",[213,2190,1817],{"class":230},[213,2192,1808],{"class":230},[213,2194,2195],{"class":377},"app_imag",[213,2197,2198],{"class":226},"e",[213,2200,1817],{"class":230},[213,2202,2203],{"class":377},":",[213,2205,2206],{"class":230},"\u003C",[213,2208,2209],{"class":377},"image_versio",[213,2211,2212],{"class":226},"n",[213,2214,1864],{"class":230},[11,2216,2217],{"id":2217},"完整示例",[75,2219,2221],{"className":1584,"code":2220,"language":1586,"meta":81,"style":81},"FROM alpine:latest\n\n# 国内源\nRUN sed -i 's\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg' \u002Fetc\u002Fapk\u002Frepositories\n\nRUN apk update --no-cache\n# 权限管理\nRUN apk add --no-cache shadow\n# 调试\nRUN apk add --no-cache bash\n# 设置时区\nRUN apk add --no-cache tzdata\n\nENV TZ=Asia\u002FShanghai\n\n# 创建用户组和用户\nRUN addgroup -S glog && adduser -S -G glog glog\n\nCOPY .\u002Fglog_static_musl_1 \u002Fapp\u002F\n\nRUN mkdir \u002Fdata\n\nVOLUME [ \"\u002Fdata\" ]\nWORKDIR \u002Fdata\n\n# 修改权限\nRUN chown -R glog:glog \u002Fdata\nRUN chown -R glog:glog \u002Fapp\n\n# 指定用户启动\nUSER glog\nCMD [ \"\u002Fapp\u002Fglog_static_musl_1\", \"logsweb\", \".\u002Fconf.ini\" ]\n",[45,2222,2223,2229,2233,2238,2251,2255,2262,2266,2272,2277,2284,2289,2296,2300,2307,2311,2315,2322,2326,2333,2337,2344,2348,2362,2369,2374,2380,2388,2396,2401,2407,2416],{"__ignoreMap":81},[213,2224,2225,2227],{"class":215,"line":216},[213,2226,1593],{"class":230},[213,2228,1948],{"class":226},[213,2230,2231],{"class":215,"line":223},[213,2232,667],{"emptyLinePlaceholder":666},[213,2234,2235],{"class":215,"line":244},[213,2236,2237],{"class":219},"# 国内源\n",[213,2239,2240,2242,2245,2248],{"class":215,"line":360},[213,2241,1646],{"class":230},[213,2243,2244],{"class":226}," sed -i ",[213,2246,2247],{"class":377},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[213,2249,2250],{"class":226}," \u002Fetc\u002Fapk\u002Frepositories\n",[213,2252,2253],{"class":215,"line":395},[213,2254,667],{"emptyLinePlaceholder":666},[213,2256,2257,2259],{"class":215,"line":412},[213,2258,1646],{"class":230},[213,2260,2261],{"class":226}," apk update --no-cache\n",[213,2263,2264],{"class":215,"line":430},[213,2265,2109],{"class":219},[213,2267,2268,2270],{"class":215,"line":442},[213,2269,1646],{"class":230},[213,2271,2116],{"class":226},[213,2273,2274],{"class":215,"line":448},[213,2275,2276],{"class":219},"# 调试\n",[213,2278,2279,2281],{"class":215,"line":461},[213,2280,1646],{"class":230},[213,2282,2283],{"class":226}," apk add --no-cache bash\n",[213,2285,2286],{"class":215,"line":751},[213,2287,2288],{"class":219},"# 设置时区\n",[213,2290,2291,2293],{"class":215,"line":765},[213,2292,1646],{"class":230},[213,2294,2295],{"class":226}," apk add --no-cache tzdata\n",[213,2297,2298],{"class":215,"line":774},[213,2299,667],{"emptyLinePlaceholder":666},[213,2301,2302,2304],{"class":215,"line":780},[213,2303,1504],{"class":230},[213,2305,2306],{"class":226}," TZ=Asia\u002FShanghai\n",[213,2308,2309],{"class":215,"line":797},[213,2310,667],{"emptyLinePlaceholder":666},[213,2312,2313],{"class":215,"line":810},[213,2314,2130],{"class":219},[213,2316,2317,2319],{"class":215,"line":817},[213,2318,1646],{"class":230},[213,2320,2321],{"class":226}," addgroup -S glog && adduser -S -G glog glog\n",[213,2323,2324],{"class":215,"line":822},[213,2325,667],{"emptyLinePlaceholder":666},[213,2327,2328,2330],{"class":215,"line":834},[213,2329,1620],{"class":230},[213,2331,2332],{"class":226}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[213,2334,2335],{"class":215,"line":842},[213,2336,667],{"emptyLinePlaceholder":666},[213,2338,2339,2341],{"class":215,"line":1714},[213,2340,1646],{"class":230},[213,2342,2343],{"class":226}," mkdir \u002Fdata\n",[213,2345,2346],{"class":215,"line":1719},[213,2347,667],{"emptyLinePlaceholder":666},[213,2349,2351,2353,2356,2359],{"class":215,"line":2350},23,[213,2352,1697],{"class":230},[213,2354,2355],{"class":226}," [ ",[213,2357,2358],{"class":377},"\"\u002Fdata\"",[213,2360,2361],{"class":226}," ]\n",[213,2363,2365,2367],{"class":215,"line":2364},24,[213,2366,1709],{"class":230},[213,2368,1700],{"class":226},[213,2370,2372],{"class":215,"line":2371},25,[213,2373,667],{"emptyLinePlaceholder":666},[213,2375,2377],{"class":215,"line":2376},26,[213,2378,2379],{"class":219},"# 修改权限\n",[213,2381,2383,2385],{"class":215,"line":2382},27,[213,2384,1646],{"class":230},[213,2386,2387],{"class":226}," chown -R glog:glog \u002Fdata\n",[213,2389,2391,2393],{"class":215,"line":2390},28,[213,2392,1646],{"class":230},[213,2394,2395],{"class":226}," chown -R glog:glog \u002Fapp\n",[213,2397,2399],{"class":215,"line":2398},29,[213,2400,667],{"emptyLinePlaceholder":666},[213,2402,2404],{"class":215,"line":2403},30,[213,2405,2406],{"class":219},"# 指定用户启动\n",[213,2408,2410,2413],{"class":215,"line":2409},31,[213,2411,2412],{"class":230},"USER",[213,2414,2415],{"class":226}," glog\n",[213,2417,2419,2421,2423,2426,2428,2430,2432,2435],{"class":215,"line":2418},32,[213,2420,1722],{"class":230},[213,2422,2355],{"class":226},[213,2424,2425],{"class":377},"\"\u002Fapp\u002Fglog_static_musl_1\"",[213,2427,381],{"class":226},[213,2429,2029],{"class":377},[213,2431,381],{"class":226},[213,2433,2434],{"class":377},"\".\u002Fconf.ini\"",[213,2436,2361],{"class":226},[11,2438,2439],{"id":2439},"总结",[15,2441,2442],{},"关键步骤：",[26,2444,2445,2451,2461,2467],{},[29,2446,2447,2450],{},[45,2448,2449],{},"apk add --no-cache shadow"," - 安装用户管理工具",[29,2452,2453,2456,2457,2460],{},[45,2454,2455],{},"addgroup"," + ",[45,2458,2459],{},"adduser"," - 创建用户",[29,2462,2463,2466],{},[45,2464,2465],{},"chown -R"," - 修改目录权限",[29,2468,2469,2472],{},[45,2470,2471],{},"USER \u003Cusername>"," - 指定运行用户",[15,2474,2475],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[1231,2477,2478],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}",{"title":81,"searchDepth":223,"depth":244,"links":2480},[2481,2482,2487,2488],{"id":2086,"depth":223,"text":2086},{"id":2092,"depth":223,"text":2092,"children":2483},[2484,2485,2486],{"id":2095,"depth":244,"text":2096},{"id":2119,"depth":244,"text":2120},{"id":2156,"depth":244,"text":2157},{"id":2217,"depth":223,"text":2217},{"id":2439,"depth":223,"text":2439},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":2081,"description":2489},"posts\u002Fdocker-user-start\u002Findex",[2075,2077,2497],"Linux","b4Uyw5ms87xr7NhpIkavHnNbT20oaEPtXz8k1w6UPj0",{"id":2500,"title":2501,"author":6,"body":2502,"description":2710,"difficulty":6,"draft":1260,"extension":1261,"image":2711,"meta":2712,"navigation":666,"path":2713,"pinned":1260,"published":2714,"seo":2715,"series":6,"seriesOrder":6,"stem":2716,"tags":2717,"updated":6,"__hash__":2719},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":8,"value":2503,"toc":2700},[2504,2508,2511,2517,2520,2524,2531,2582,2586,2592,2598,2602,2605,2608,2678,2680,2691,2697],[11,2505,2507],{"id":2506},"什么是-sveltekit","什么是 SvelteKit？",[15,2509,2510],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[15,2512,2513],{},[1540,2514],{"alt":2515,"src":2516},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[11,2518,2519],{"id":2519},"核心特点",[113,2521,2523],{"id":2522},"_1-编译时框架","1. 编译时框架",[15,2525,2526,2527,2530],{},"Svelte 和 React\u002FVue 最大的区别在于：",[19,2528,2529],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[75,2532,2536],{"className":2533,"code":2534,"language":2535,"meta":81,"style":81},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n","typescript",[45,2537,2538,2543,2556,2567,2578],{"__ignoreMap":81},[213,2539,2540],{"class":215,"line":216},[213,2541,2542],{"class":219},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[213,2544,2545,2548,2551,2553],{"class":215,"line":223},[213,2546,2547],{"class":230},"let",[213,2549,2550],{"class":226}," count ",[213,2552,1386],{"class":230},[213,2554,2555],{"class":339}," 0\n",[213,2557,2558,2561,2564],{"class":215,"line":244},[213,2559,2560],{"class":230},"function",[213,2562,2563],{"class":237}," increment",[213,2565,2566],{"class":226},"() {\n",[213,2568,2569,2572,2575],{"class":215,"line":360},[213,2570,2571],{"class":226},"  count ",[213,2573,2574],{"class":230},"+=",[213,2576,2577],{"class":339}," 1\n",[213,2579,2580],{"class":215,"line":395},[213,2581,445],{"class":226},[113,2583,2585],{"id":2584},"_2-文件路由","2. 文件路由",[15,2587,2588,2591],{},[45,2589,2590],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[75,2593,2596],{"className":2594,"code":2595,"language":80},[78],"src\u002Froutes\u002F\n├── +page.svelte        → \u002F\n├── about\u002F\n│   └── +page.svelte    → \u002Fabout\n└── posts\u002F\n    ├── +page.svelte    → \u002Fposts\n    └── [slug]\u002F\n        └── +page.svelte → \u002Fposts\u002F:slug\n",[45,2597,2595],{"__ignoreMap":81},[113,2599,2601],{"id":2600},"_3-全栈能力","3. 全栈能力",[15,2603,2604],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[11,2606,2607],{"id":2607},"和其他框架对比",[249,2609,2610,2626],{},[252,2611,2612],{},[255,2613,2614,2617,2620,2623],{},[258,2615,2616],{},"特性",[258,2618,2619],{},"SvelteKit",[258,2621,2622],{},"Next.js",[258,2624,2625],{},"Nuxt",[265,2627,2628,2641,2654,2667],{},[255,2629,2630,2633,2636,2639],{},[270,2631,2632],{},"学习曲线",[270,2634,2635],{},"低",[270,2637,2638],{},"中",[270,2640,2638],{},[255,2642,2643,2646,2649,2652],{},[270,2644,2645],{},"包体积",[270,2647,2648],{},"小",[270,2650,2651],{},"大",[270,2653,2638],{},[255,2655,2656,2659,2662,2665],{},[270,2657,2658],{},"运行时开销",[270,2660,2661],{},"无",[270,2663,2664],{},"有",[270,2666,2664],{},[255,2668,2669,2672,2674,2676],{},[270,2670,2671],{},"生态",[270,2673,2648],{},[270,2675,2651],{},[270,2677,2651],{},[11,2679,2439],{"id":2439},[15,2681,2682,2683,2686,2687,2690],{},"如果你追求",[19,2684,2685],{},"简洁的代码","和",[19,2688,2689],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[15,2692,2693],{},[1540,2694],{"alt":2695,"src":2696},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[1231,2698,2699],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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":81,"searchDepth":223,"depth":244,"links":2701},[2702,2703,2708,2709],{"id":2506,"depth":223,"text":2507},{"id":2519,"depth":223,"text":2519,"children":2704},[2705,2706,2707],{"id":2522,"depth":244,"text":2523},{"id":2584,"depth":244,"text":2585},{"id":2600,"depth":244,"text":2601},{"id":2607,"depth":223,"text":2607},{"id":2439,"depth":223,"text":2439},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":2501,"description":2710},"posts\u002Fsveltekit-intro\u002Findex",[2619,2718],"前端框架","vsEGyunpCWR7DiJAdyV1SPDfa57xXLITBI-lKM8UeGQ",{"id":2721,"title":2722,"author":6,"body":2723,"description":3124,"difficulty":1259,"draft":1260,"extension":1261,"image":3125,"meta":3126,"navigation":666,"path":3127,"pinned":666,"published":3128,"seo":3129,"series":3130,"seriesOrder":216,"stem":3131,"tags":3132,"updated":3134,"__hash__":3135},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎使用 SuBlog",{"type":8,"value":2724,"toc":3115},[2725,2729,2736,2739,2742,2745,2819,2822,2825,2887,2890,2893,2958,2961,3027,3030,3033,3049,3056,3059,3090,3093,3104,3107,3112],[11,2726,2728],{"id":2727},"博客上线啦","博客上线啦！",[15,2730,2731,2732,2735],{},"Hello World！欢迎使用 ",[19,2733,2734],{},"SuBlog","，这是站点的第一篇示例文章。",[15,2737,2738],{},"经过一段时间的折腾，这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。",[11,2740,2741],{"id":2741},"技术栈",[15,2743,2744],{},"本站使用了以下技术：",[249,2746,2747,2757],{},[252,2748,2749],{},[255,2750,2751,2754],{},[258,2752,2753],{},"技术",[258,2755,2756],{},"用途",[265,2758,2759,2769,2779,2789,2799,2809],{},[255,2760,2761,2766],{},[270,2762,2763],{},[19,2764,2765],{},"Nuxt 3",[270,2767,2768],{},"Vue 3 全栈框架，提供 SSG、路由、API 等能力",[255,2770,2771,2776],{},[270,2772,2773],{},[19,2774,2775],{},"TypeScript",[270,2777,2778],{},"类型安全，提升开发体验",[255,2780,2781,2786],{},[270,2782,2783],{},[19,2784,2785],{},"Tailwind CSS v4",[270,2787,2788],{},"原子化 CSS，快速构建 UI",[255,2790,2791,2796],{},[270,2792,2793],{},[19,2794,2795],{},"@nuxt\u002Fcontent",[270,2797,2798],{},"Markdown 驱动的内容管理",[255,2800,2801,2806],{},[270,2802,2803],{},[19,2804,2805],{},"Shiki",[270,2807,2808],{},"代码语法高亮",[255,2810,2811,2816],{},[270,2812,2813],{},[19,2814,2815],{},"Giscus",[270,2817,2818],{},"基于 GitHub Discussions 的评论系统",[11,2820,2821],{"id":2821},"功能特性",[15,2823,2824],{},"目前已实现的功能：",[177,2826,2827,2833,2839,2845,2851,2857,2863,2869,2875,2881],{},[29,2828,2829,2832],{},[19,2830,2831],{},"博客系统"," - Markdown 文章，支持代码高亮、目录导航、搜索",[29,2834,2835,2838],{},[19,2836,2837],{},"暗色模式"," - 跟随系统或手动切换",[29,2840,2841,2844],{},[19,2842,2843],{},"文章搜索"," - 支持标题、描述、正文、标签多维度搜索",[29,2846,2847,2850],{},[19,2848,2849],{},"标签系统"," - 文章标签分类，高频标签快速筛选",[29,2852,2853,2856],{},[19,2854,2855],{},"友链页面"," - 展示友情链接，支持申请",[29,2858,2859,2862],{},[19,2860,2861],{},"赞助页面"," - 微信收款码展示",[29,2864,2865,2868],{},[19,2866,2867],{},"封面制作"," - 在线生成封面图片工具",[29,2870,2871,2874],{},[19,2872,2873],{},"网易云热评"," - 首页随机展示音乐评论",[29,2876,2877,2880],{},[19,2878,2879],{},"访客统计"," - 不蒜子计数器，带数字滚动动画",[29,2882,2883,2886],{},[19,2884,2885],{},"SEO 优化"," - 自动生成站点地图、Open Graph 标签",[11,2888,2889],{"id":2889},"代码示例",[15,2891,2892],{},"本站的配置非常集中，一个文件搞定全站定制：",[75,2894,2896],{"className":2533,"code":2895,"language":2535,"meta":81,"style":81},"\u002F\u002F config\u002Fsite.ts\nexport const siteConfig = {\n  siteName: '我的博客',\n  url: 'https:\u002F\u002Fexample.com\u002F',\n  avatar: 'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100',\n  \u002F\u002F ... 改这一个文件，全站生效\n}\n",[45,2897,2898,2903,2918,2929,2939,2949,2954],{"__ignoreMap":81},[213,2899,2900],{"class":215,"line":216},[213,2901,2902],{"class":219},"\u002F\u002F config\u002Fsite.ts\n",[213,2904,2905,2907,2910,2913,2916],{"class":215,"line":223},[213,2906,1380],{"class":230},[213,2908,2909],{"class":230}," const",[213,2911,2912],{"class":339}," siteConfig",[213,2914,2915],{"class":230}," =",[213,2917,427],{"class":226},[213,2919,2920,2923,2926],{"class":215,"line":244},[213,2921,2922],{"class":226},"  siteName: ",[213,2924,2925],{"class":377},"'我的博客'",[213,2927,2928],{"class":226},",\n",[213,2930,2931,2934,2937],{"class":215,"line":360},[213,2932,2933],{"class":226},"  url: ",[213,2935,2936],{"class":377},"'https:\u002F\u002Fexample.com\u002F'",[213,2938,2928],{"class":226},[213,2940,2941,2944,2947],{"class":215,"line":395},[213,2942,2943],{"class":226},"  avatar: ",[213,2945,2946],{"class":377},"'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100'",[213,2948,2928],{"class":226},[213,2950,2951],{"class":215,"line":412},[213,2952,2953],{"class":219},"  \u002F\u002F ... 改这一个文件，全站生效\n",[213,2955,2956],{"class":215,"line":430},[213,2957,445],{"class":226},[15,2959,2960],{},"文章的 frontmatter 也很简洁：",[75,2962,2966],{"className":2963,"code":2964,"language":2965,"meta":81,"style":81},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 文章标题\npublished: 2026-05-31T08:00:00\ndescription: 文章简介\ntags: ['Nuxt', 'TypeScript']\n---\n","yaml",[45,2967,2968,2973,2985,2995,3005,3023],{"__ignoreMap":81},[213,2969,2970],{"class":215,"line":216},[213,2971,2972],{"class":237},"---\n",[213,2974,2975,2979,2982],{"class":215,"line":223},[213,2976,2978],{"class":2977},"sbB4o","title",[213,2980,2981],{"class":226},": ",[213,2983,2984],{"class":377},"文章标题\n",[213,2986,2987,2990,2992],{"class":215,"line":244},[213,2988,2989],{"class":2977},"published",[213,2991,2981],{"class":226},[213,2993,2994],{"class":339},"2026-05-31T08:00:00\n",[213,2996,2997,3000,3002],{"class":215,"line":360},[213,2998,2999],{"class":2977},"description",[213,3001,2981],{"class":226},[213,3003,3004],{"class":377},"文章简介\n",[213,3006,3007,3010,3013,3016,3018,3021],{"class":215,"line":395},[213,3008,3009],{"class":2977},"tags",[213,3011,3012],{"class":226},": [",[213,3014,3015],{"class":377},"'Nuxt'",[213,3017,381],{"class":226},[213,3019,3020],{"class":377},"'TypeScript'",[213,3022,703],{"class":226},[213,3024,3025],{"class":215,"line":412},[213,3026,2972],{"class":237},[11,3028,3029],{"id":3029},"部署",[15,3031,3032],{},"本站使用 SSG 静态生成，构建后直接部署到静态托管即可：",[75,3034,3036],{"className":1291,"code":3035,"language":1293,"meta":81,"style":81},"npx nuxi generate\n",[45,3037,3038],{"__ignoreMap":81},[213,3039,3040,3043,3046],{"class":215,"line":216},[213,3041,3042],{"class":237},"npx",[213,3044,3045],{"class":377}," nuxi",[213,3047,3048],{"class":377}," generate\n",[15,3050,3051,3052,3055],{},"构建产物在 ",[45,3053,3054],{},".output\u002Fpublic"," 目录下，可以直接丢到 Nginx、Vercel、Netlify 等平台。",[11,3057,3058],{"id":3058},"后续计划",[177,3060,3063,3072,3078,3084],{"className":3061},[3062],"contains-task-list",[29,3064,3067,3071],{"className":3065},[3066],"task-list-item",[3068,3069],"input",{"disabled":666,"type":3070},"checkbox"," RSS 订阅",[29,3073,3075,3077],{"className":3074},[3066],[3068,3076],{"disabled":666,"type":3070}," 文章目录优化",[29,3079,3081,3083],{"className":3080},[3066],[3068,3082],{"disabled":666,"type":3070}," 更多小工具",[29,3085,3087,3089],{"className":3086},[3066],[3068,3088],{"disabled":666,"type":3070}," 持续输出内容",[11,3091,3092],{"id":3092},"致谢",[15,3094,3095,3096,3103],{},"感谢 ",[3097,3098,3102],"a",{"href":3099,"rel":3100},"https:\u002F\u002F2x.nz",[3101],"nofollow","二叉树树"," 的开源项目，本站的 UI 设计参考了他的项目。",[3105,3106],"hr",{},[15,3108,3109],{},[19,3110,3111],{},"感谢阅读！如果觉得不错，欢迎点个 Star 或者留个评论 (☞ﾟヮﾟ)☞",[1231,3113,3114],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":81,"searchDepth":223,"depth":244,"links":3116},[3117,3118,3119,3120,3121,3122,3123],{"id":2727,"depth":223,"text":2728},{"id":2741,"depth":223,"text":2741},{"id":2821,"depth":223,"text":2821},{"id":2889,"depth":223,"text":2889},{"id":3029,"depth":223,"text":3029},{"id":3058,"depth":223,"text":3058},{"id":3092,"depth":223,"text":3092},"博客上线啦！这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。","\u002Fposts\u002Fhello-world\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fhello-world","2026-05-31T08:00:00",{"title":2722,"description":3124},"Nuxt 博客开发","posts\u002Fhello-world\u002Findex",[3133,2625],"博客","2026-05-31","f9MCboCBfQIUxT8aKv6aKGDFxysiG8ZG0YXr5X5LTFo",{"id":3137,"title":3138,"author":6,"body":3139,"description":4303,"difficulty":2066,"draft":1260,"extension":1261,"image":4304,"meta":4305,"navigation":666,"path":4306,"pinned":1260,"published":4307,"seo":4308,"series":3130,"seriesOrder":223,"stem":4309,"tags":4310,"updated":3134,"__hash__":4313},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",{"type":8,"value":3140,"toc":4293},[3141,3145,3148,3151,3155,3158,3168,3171,3178,3244,3251,3253,3260,3534,3544,3548,3551,3554,3579,3593,3596,3619,3625,3628,3631,3634,3639,3646,3872,3877,3921,3925,3928,3935,3938,3986,3992,4220,4230,4232,4284,4287,4290],[3142,3143,3144],"h1",{"id":3144},"背景",[15,3146,3147],{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[15,3149,3150],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[3142,3152,3154],{"id":3153},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[11,3156,3157],{"id":3157},"现象",[15,3159,3160,3161,3164,3165,59],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[45,3162,3163],{},"post.body.toc"," 上。直接用它渲染目录，发现",[19,3166,3167],{},"只有 h2 标题，h3 全部丢失",[11,3169,3170],{"id":3170},"排查",[15,3172,3173,3174,3177],{},"查了官方文档，发现 ",[45,3175,3176],{},"build.markdown.toc.depth"," 可以控制深度：",[75,3179,3181],{"className":2533,"code":3180,"language":2535,"meta":81,"style":81},"\u002F\u002F nuxt.config.ts\ncontent: {\n  build: {\n    markdown: {\n      toc: { depth: 3 }  \u002F\u002F 包含 h3\n    }\n  }\n}\n",[45,3182,3183,3188,3196,3203,3210,3231,3235,3240],{"__ignoreMap":81},[213,3184,3185],{"class":215,"line":216},[213,3186,3187],{"class":219},"\u002F\u002F nuxt.config.ts\n",[213,3189,3190,3193],{"class":215,"line":223},[213,3191,3192],{"class":237},"content",[213,3194,3195],{"class":226},": {\n",[213,3197,3198,3201],{"class":215,"line":244},[213,3199,3200],{"class":237},"  build",[213,3202,3195],{"class":226},[213,3204,3205,3208],{"class":215,"line":360},[213,3206,3207],{"class":237},"    markdown",[213,3209,3195],{"class":226},[213,3211,3212,3215,3218,3221,3223,3225,3228],{"class":215,"line":395},[213,3213,3214],{"class":237},"      toc",[213,3216,3217],{"class":226},": { ",[213,3219,3220],{"class":237},"depth",[213,3222,2981],{"class":226},[213,3224,340],{"class":339},[213,3226,3227],{"class":226}," }  ",[213,3229,3230],{"class":219},"\u002F\u002F 包含 h3\n",[213,3232,3233],{"class":215,"line":412},[213,3234,777],{"class":226},[213,3236,3237],{"class":215,"line":430},[213,3238,3239],{"class":226},"  }\n",[213,3241,3242],{"class":215,"line":442},[213,3243,445],{"class":226},[15,3245,3246,3247,3250],{},"配置改了，清了缓存重启，",[19,3248,3249],{},"依然只有 h2","。这个配置似乎不起作用。",[11,3252,1560],{"id":1560},[15,3254,3255,3256,3259],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[45,3257,3258],{},"post.body"," 的 AST 里递归提取所有标题：",[75,3261,3263],{"className":2533,"code":3262,"language":2535,"meta":81,"style":81},"function extractHeadings(node: any, list: Heading[] = []): Heading[] {\n  if (!node) return list\n  if (Array.isArray(node)) {\n    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n    if (typeof node[0] === 'string' && \u002F^h[1-6]$\u002F.test(node[0])) {\n      const level = Number(node[0].charAt(1))\n      const text = extractText(node.slice(2)).trim()\n      list.push({ id: node[1]?.id || slugify(text), text, level })\n    }\n    for (const child of node) extractHeadings(child, list)\n  }\n  return list\n}\n",[45,3264,3265,3307,3327,3340,3345,3404,3434,3465,3490,3494,3519,3523,3530],{"__ignoreMap":81},[213,3266,3267,3269,3272,3274,3277,3279,3282,3284,3287,3289,3292,3295,3297,3300,3302,3304],{"class":215,"line":216},[213,3268,2560],{"class":230},[213,3270,3271],{"class":237}," extractHeadings",[213,3273,739],{"class":226},[213,3275,3276],{"class":865},"node",[213,3278,2203],{"class":230},[213,3280,3281],{"class":339}," any",[213,3283,381],{"class":226},[213,3285,3286],{"class":865},"list",[213,3288,2203],{"class":230},[213,3290,3291],{"class":237}," Heading",[213,3293,3294],{"class":226},"[] ",[213,3296,1386],{"class":230},[213,3298,3299],{"class":226}," [])",[213,3301,2203],{"class":230},[213,3303,3291],{"class":237},[213,3305,3306],{"class":226},"[] {\n",[213,3308,3309,3312,3315,3318,3321,3324],{"class":215,"line":223},[213,3310,3311],{"class":230},"  if",[213,3313,3314],{"class":226}," (",[213,3316,3317],{"class":230},"!",[213,3319,3320],{"class":226},"node) ",[213,3322,3323],{"class":230},"return",[213,3325,3326],{"class":226}," list\n",[213,3328,3329,3331,3334,3337],{"class":215,"line":244},[213,3330,3311],{"class":230},[213,3332,3333],{"class":226}," (Array.",[213,3335,3336],{"class":237},"isArray",[213,3338,3339],{"class":226},"(node)) {\n",[213,3341,3342],{"class":215,"line":360},[213,3343,3344],{"class":219},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[213,3346,3347,3349,3351,3354,3357,3360,3363,3366,3369,3372,3375,3378,3382,3385,3388,3391,3393,3396,3399,3401],{"class":215,"line":395},[213,3348,754],{"class":230},[213,3350,3314],{"class":226},[213,3352,3353],{"class":230},"typeof",[213,3355,3356],{"class":226}," node[",[213,3358,3359],{"class":339},"0",[213,3361,3362],{"class":226},"] ",[213,3364,3365],{"class":230},"===",[213,3367,3368],{"class":377}," 'string'",[213,3370,3371],{"class":230}," &&",[213,3373,3374],{"class":377}," \u002F",[213,3376,3377],{"class":230},"^",[213,3379,3381],{"class":3380},"sPKmS","h",[213,3383,3384],{"class":339},"[1-6]",[213,3386,3387],{"class":230},"$",[213,3389,3390],{"class":377},"\u002F",[213,3392,686],{"class":226},[213,3394,3395],{"class":237},"test",[213,3397,3398],{"class":226},"(node[",[213,3400,3359],{"class":339},[213,3402,3403],{"class":226},"])) {\n",[213,3405,3406,3409,3412,3414,3417,3419,3421,3424,3427,3429,3431],{"class":215,"line":412},[213,3407,3408],{"class":230},"      const",[213,3410,3411],{"class":339}," level",[213,3413,2915],{"class":230},[213,3415,3416],{"class":237}," Number",[213,3418,3398],{"class":226},[213,3420,3359],{"class":339},[213,3422,3423],{"class":226},"].",[213,3425,3426],{"class":237},"charAt",[213,3428,739],{"class":226},[213,3430,1404],{"class":339},[213,3432,3433],{"class":226},"))\n",[213,3435,3436,3438,3441,3443,3446,3449,3452,3454,3457,3460,3463],{"class":215,"line":430},[213,3437,3408],{"class":230},[213,3439,3440],{"class":339}," text",[213,3442,2915],{"class":230},[213,3444,3445],{"class":237}," extractText",[213,3447,3448],{"class":226},"(node.",[213,3450,3451],{"class":237},"slice",[213,3453,739],{"class":226},[213,3455,3456],{"class":339},"2",[213,3458,3459],{"class":226},")).",[213,3461,3462],{"class":237},"trim",[213,3464,357],{"class":226},[213,3466,3467,3470,3473,3476,3478,3481,3484,3487],{"class":215,"line":442},[213,3468,3469],{"class":226},"      list.",[213,3471,3472],{"class":237},"push",[213,3474,3475],{"class":226},"({ id: node[",[213,3477,1404],{"class":339},[213,3479,3480],{"class":226},"]?.id ",[213,3482,3483],{"class":230},"||",[213,3485,3486],{"class":237}," slugify",[213,3488,3489],{"class":226},"(text), text, level })\n",[213,3491,3492],{"class":215,"line":448},[213,3493,777],{"class":226},[213,3495,3496,3499,3501,3504,3507,3510,3513,3516],{"class":215,"line":461},[213,3497,3498],{"class":230},"    for",[213,3500,3314],{"class":226},[213,3502,3503],{"class":230},"const",[213,3505,3506],{"class":339}," child",[213,3508,3509],{"class":230}," of",[213,3511,3512],{"class":226}," node) ",[213,3514,3515],{"class":237},"extractHeadings",[213,3517,3518],{"class":226},"(child, list)\n",[213,3520,3521],{"class":215,"line":751},[213,3522,3239],{"class":226},[213,3524,3525,3528],{"class":215,"line":765},[213,3526,3527],{"class":230},"  return",[213,3529,3326],{"class":226},[213,3531,3532],{"class":215,"line":774},[213,3533,445],{"class":226},[15,3535,3536,3537,3539,3540,3543],{},"关键点：Nuxt Content v3 的 body 使用 ",[19,3538,8],{}," 格式，标题节点是数组 ",[45,3541,3542],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[3142,3545,3547],{"id":3546},"问题二图片路径解析错误","问题二：图片路径解析错误",[11,3549,3157],{"id":3550},"现象-1",[15,3552,3553],{},"markdown 里用相对路径引用图片：",[75,3555,3559],{"className":3556,"code":3557,"language":3558,"meta":81,"style":81},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[45,3560,3561],{"__ignoreMap":81},[213,3562,3563,3566,3570,3573,3577],{"class":215,"line":216},[213,3564,3565],{"class":226},"![",[213,3567,3569],{"class":3568},"smiIW","SSHFS挂载效果",[213,3571,3572],{"class":226},"](",[213,3574,3576],{"class":3575},"szbl0","img\u002Fsshfs.avif",[213,3578,392],{"class":226},[15,3580,3581,3582,3585,3586,3589,3590,59],{},"HTML 输出的 ",[45,3583,3584],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[45,3587,3588],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[45,3591,3592],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[11,3594,3170],{"id":3595},"排查-1",[26,3597,3598,3605,3608,3614],{},[29,3599,3600,3601,3604],{},"检查了 ",[45,3602,3603],{},"\u003Cbase>"," 标签 —— 没有",[29,3606,3607],{},"检查了重定向 —— 没有",[29,3609,3610,3611,3613],{},"直接访问 ",[45,3612,3592],{}," —— 返回 200 OK",[29,3615,3610,3616,3618],{},[45,3617,3588],{}," —— 404",[15,3620,3621,3622,59],{},"服务器路由没问题，问题是",[19,3623,3624],{},"浏览器解析相对路径时基于了错误的基准 URL",[15,3626,3627],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[11,3629,1560],{"id":3630},"最终方案-1",[15,3632,3633],{},"两步解决：",[15,3635,3636],{},[19,3637,3638],{},"第一步：自定义 ProseImg 组件",[15,3640,3641,3642,3645],{},"创建 ",[45,3643,3644],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[75,3647,3651],{"className":3648,"code":3649,"language":3650,"meta":81,"style":81},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nconst route = useRoute()\nconst resolvedSrc = computed(() => {\n  if (!props.src) return ''\n  if (props.src.startsWith('\u002F') || props.src.startsWith('http')) return props.src\n  const base = route.path.replace(\u002F\\\u002F$\u002F, '')\n  return `${base}\u002F${props.src}`\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cimg :src=\"resolvedSrc\" :alt=\"alt\" \u002F>\n\u003C\u002Ftemplate>\n","vue",[45,3652,3653,3665,3679,3699,3715,3752,3787,3811,3816,3825,3829,3838,3864],{"__ignoreMap":81},[213,3654,3655,3657,3660,3663],{"class":215,"line":216},[213,3656,2206],{"class":226},[213,3658,3659],{"class":2977},"script",[213,3661,3662],{"class":237}," setup",[213,3664,1864],{"class":226},[213,3666,3667,3669,3672,3674,3677],{"class":215,"line":223},[213,3668,3503],{"class":230},[213,3670,3671],{"class":339}," route",[213,3673,2915],{"class":230},[213,3675,3676],{"class":237}," useRoute",[213,3678,357],{"class":226},[213,3680,3681,3683,3686,3688,3691,3694,3697],{"class":215,"line":244},[213,3682,3503],{"class":230},[213,3684,3685],{"class":339}," resolvedSrc",[213,3687,2915],{"class":230},[213,3689,3690],{"class":237}," computed",[213,3692,3693],{"class":226},"(() ",[213,3695,3696],{"class":230},"=>",[213,3698,427],{"class":226},[213,3700,3701,3703,3705,3707,3710,3712],{"class":215,"line":360},[213,3702,3311],{"class":230},[213,3704,3314],{"class":226},[213,3706,3317],{"class":230},[213,3708,3709],{"class":226},"props.src) ",[213,3711,3323],{"class":230},[213,3713,3714],{"class":377}," ''\n",[213,3716,3717,3719,3722,3725,3727,3730,3732,3734,3737,3739,3741,3744,3747,3749],{"class":215,"line":395},[213,3718,3311],{"class":230},[213,3720,3721],{"class":226}," (props.src.",[213,3723,3724],{"class":237},"startsWith",[213,3726,739],{"class":226},[213,3728,3729],{"class":377},"'\u002F'",[213,3731,745],{"class":226},[213,3733,3483],{"class":230},[213,3735,3736],{"class":226}," props.src.",[213,3738,3724],{"class":237},[213,3740,739],{"class":226},[213,3742,3743],{"class":377},"'http'",[213,3745,3746],{"class":226},")) ",[213,3748,3323],{"class":230},[213,3750,3751],{"class":226}," props.src\n",[213,3753,3754,3757,3760,3762,3765,3768,3770,3772,3776,3778,3780,3782,3785],{"class":215,"line":412},[213,3755,3756],{"class":230},"  const",[213,3758,3759],{"class":339}," base",[213,3761,2915],{"class":230},[213,3763,3764],{"class":226}," route.path.",[213,3766,3767],{"class":237},"replace",[213,3769,739],{"class":226},[213,3771,3390],{"class":377},[213,3773,3775],{"class":3774},"sCnZR","\\\u002F",[213,3777,3387],{"class":230},[213,3779,3390],{"class":377},[213,3781,381],{"class":226},[213,3783,3784],{"class":377},"''",[213,3786,392],{"class":226},[213,3788,3789,3791,3794,3797,3800,3803,3805,3808],{"class":215,"line":430},[213,3790,3527],{"class":230},[213,3792,3793],{"class":377}," `${",[213,3795,3796],{"class":226},"base",[213,3798,3799],{"class":377},"}\u002F${",[213,3801,3802],{"class":226},"props",[213,3804,686],{"class":377},[213,3806,3807],{"class":226},"src",[213,3809,3810],{"class":377},"}`\n",[213,3812,3813],{"class":215,"line":442},[213,3814,3815],{"class":226},"})\n",[213,3817,3818,3821,3823],{"class":215,"line":448},[213,3819,3820],{"class":226},"\u003C\u002F",[213,3822,3659],{"class":2977},[213,3824,1864],{"class":226},[213,3826,3827],{"class":215,"line":461},[213,3828,667],{"emptyLinePlaceholder":666},[213,3830,3831,3833,3836],{"class":215,"line":751},[213,3832,2206],{"class":226},[213,3834,3835],{"class":2977},"template",[213,3837,1864],{"class":226},[213,3839,3840,3843,3845,3848,3850,3853,3856,3858,3861],{"class":215,"line":765},[213,3841,3842],{"class":226},"  \u003C",[213,3844,1540],{"class":2977},[213,3846,3847],{"class":237}," :src",[213,3849,1386],{"class":226},[213,3851,3852],{"class":377},"\"resolvedSrc\"",[213,3854,3855],{"class":237}," :alt",[213,3857,1386],{"class":226},[213,3859,3860],{"class":377},"\"alt\"",[213,3862,3863],{"class":226}," \u002F>\n",[213,3865,3866,3868,3870],{"class":215,"line":774},[213,3867,3820],{"class":226},[213,3869,3835],{"class":2977},[213,3871,1864],{"class":226},[15,3873,3874],{},[19,3875,3876],{},"第二步：启用 MDC prose 组件",[75,3878,3880],{"className":2533,"code":3879,"language":2535,"meta":81,"style":81},"\u002F\u002F nuxt.config.ts\nmdc: {\n  components: {\n    prose: true  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n  }\n}\n",[45,3881,3882,3886,3893,3900,3913,3917],{"__ignoreMap":81},[213,3883,3884],{"class":215,"line":216},[213,3885,3187],{"class":219},[213,3887,3888,3891],{"class":215,"line":223},[213,3889,3890],{"class":237},"mdc",[213,3892,3195],{"class":226},[213,3894,3895,3898],{"class":215,"line":244},[213,3896,3897],{"class":237},"  components",[213,3899,3195],{"class":226},[213,3901,3902,3905,3907,3910],{"class":215,"line":360},[213,3903,3904],{"class":237},"    prose",[213,3906,2981],{"class":226},[213,3908,3909],{"class":339},"true",[213,3911,3912],{"class":219},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[213,3914,3915],{"class":215,"line":395},[213,3916,3239],{"class":226},[213,3918,3919],{"class":215,"line":412},[213,3920,445],{"class":226},[3142,3922,3924],{"id":3923},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[11,3926,3157],{"id":3927},"现象-2",[15,3929,3930,3931,3934],{},"原站（SvelteKit）的图片放在 ",[45,3932,3933],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[11,3936,3937],{"id":3937},"方案对比",[249,3939,3940,3951],{},[252,3941,3942],{},[255,3943,3944,3947,3949],{},[258,3945,3946],{},"方案",[258,3948,260],{},[258,3950,263],{},[265,3952,3953,3964,3975],{},[255,3954,3955,3958,3961],{},[270,3956,3957],{},"复制到 public\u002F",[270,3959,3960],{},"简单",[270,3962,3963],{},"文件重复，需同步",[255,3965,3966,3969,3972],{},[270,3967,3968],{},"Vite 插件拦截",[270,3970,3971],{},"原站方案",[270,3973,3974],{},"Nuxt 不直接支持",[255,3976,3977,3980,3983],{},[270,3978,3979],{},"Server Route",[270,3981,3982],{},"不复制文件",[270,3984,3985],{},"需要手动写路由",[15,3987,3988,3989,3991],{},"最终选择 ",[19,3990,3979],{},"，和原站的 Vite 插件思路一致：",[75,3993,3995],{"className":2533,"code":3994,"language":2535,"meta":81,"style":81},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\nexport default defineEventHandler(async (event) => {\n  const url = getRequestURL(event)\n  const match = url.pathname.match(\u002F^\\\u002Fposts\\\u002F([^\u002F]+)\\\u002Fimg\\\u002F(.+)$\u002F)\n  if (!match) return\n\n  const [, slug, filename] = match\n  const filePath = resolve('content\u002Fposts', slug, 'img', filename)\n  const data = await readFile(filePath)\n  setResponseHeader(event, 'content-type', MIME[ext])\n  return data\n})\n",[45,3996,3997,4002,4028,4043,4106,4120,4124,4146,4172,4190,4209,4216],{"__ignoreMap":81},[213,3998,3999],{"class":215,"line":216},[213,4000,4001],{"class":219},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[213,4003,4004,4006,4009,4012,4014,4017,4019,4022,4024,4026],{"class":215,"line":223},[213,4005,1380],{"class":230},[213,4007,4008],{"class":230}," default",[213,4010,4011],{"class":237}," defineEventHandler",[213,4013,739],{"class":226},[213,4015,4016],{"class":230},"async",[213,4018,3314],{"class":226},[213,4020,4021],{"class":865},"event",[213,4023,745],{"class":226},[213,4025,3696],{"class":230},[213,4027,427],{"class":226},[213,4029,4030,4032,4035,4037,4040],{"class":215,"line":244},[213,4031,3756],{"class":230},[213,4033,4034],{"class":339}," url",[213,4036,2915],{"class":230},[213,4038,4039],{"class":237}," getRequestURL",[213,4041,4042],{"class":226},"(event)\n",[213,4044,4045,4047,4050,4052,4055,4058,4060,4062,4064,4066,4069,4071,4073,4075,4077,4080,4083,4086,4088,4090,4092,4094,4096,4098,4100,4102,4104],{"class":215,"line":360},[213,4046,3756],{"class":230},[213,4048,4049],{"class":339}," match",[213,4051,2915],{"class":230},[213,4053,4054],{"class":226}," url.pathname.",[213,4056,4057],{"class":237},"match",[213,4059,739],{"class":226},[213,4061,3390],{"class":377},[213,4063,3377],{"class":230},[213,4065,3775],{"class":3774},[213,4067,4068],{"class":3380},"posts",[213,4070,3775],{"class":3774},[213,4072,739],{"class":3380},[213,4074,692],{"class":339},[213,4076,3377],{"class":230},[213,4078,4079],{"class":339},"\u002F]",[213,4081,4082],{"class":230},"+",[213,4084,4085],{"class":3380},")",[213,4087,3775],{"class":3774},[213,4089,1540],{"class":3380},[213,4091,3775],{"class":3774},[213,4093,739],{"class":3380},[213,4095,686],{"class":339},[213,4097,4082],{"class":230},[213,4099,4085],{"class":3380},[213,4101,3387],{"class":230},[213,4103,3390],{"class":377},[213,4105,392],{"class":226},[213,4107,4108,4110,4112,4114,4117],{"class":215,"line":395},[213,4109,3311],{"class":230},[213,4111,3314],{"class":226},[213,4113,3317],{"class":230},[213,4115,4116],{"class":226},"match) ",[213,4118,4119],{"class":230},"return\n",[213,4121,4122],{"class":215,"line":412},[213,4123,667],{"emptyLinePlaceholder":666},[213,4125,4126,4128,4131,4134,4136,4139,4141,4143],{"class":215,"line":430},[213,4127,3756],{"class":230},[213,4129,4130],{"class":226}," [, ",[213,4132,4133],{"class":339},"slug",[213,4135,381],{"class":226},[213,4137,4138],{"class":339},"filename",[213,4140,3362],{"class":226},[213,4142,1386],{"class":230},[213,4144,4145],{"class":226}," match\n",[213,4147,4148,4150,4153,4155,4158,4160,4163,4166,4169],{"class":215,"line":442},[213,4149,3756],{"class":230},[213,4151,4152],{"class":339}," filePath",[213,4154,2915],{"class":230},[213,4156,4157],{"class":237}," resolve",[213,4159,739],{"class":226},[213,4161,4162],{"class":377},"'content\u002Fposts'",[213,4164,4165],{"class":226},", slug, ",[213,4167,4168],{"class":377},"'img'",[213,4170,4171],{"class":226},", filename)\n",[213,4173,4174,4176,4179,4181,4184,4187],{"class":215,"line":448},[213,4175,3756],{"class":230},[213,4177,4178],{"class":339}," data",[213,4180,2915],{"class":230},[213,4182,4183],{"class":230}," await",[213,4185,4186],{"class":237}," readFile",[213,4188,4189],{"class":226},"(filePath)\n",[213,4191,4192,4195,4198,4201,4203,4206],{"class":215,"line":461},[213,4193,4194],{"class":237},"  setResponseHeader",[213,4196,4197],{"class":226},"(event, ",[213,4199,4200],{"class":377},"'content-type'",[213,4202,381],{"class":226},[213,4204,4205],{"class":339},"MIME",[213,4207,4208],{"class":226},"[ext])\n",[213,4210,4211,4213],{"class":215,"line":751},[213,4212,3527],{"class":230},[213,4214,4215],{"class":226}," data\n",[213,4217,4218],{"class":215,"line":765},[213,4219,3815],{"class":226},[15,4221,4222,4223,4225,4226,4229],{},"请求 ",[45,4224,3592],{}," → 读取 ",[45,4227,4228],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[3142,4231,2439],{"id":2439},[249,4233,4234,4246],{},[252,4235,4236],{},[255,4237,4238,4241,4244],{},[258,4239,4240],{},"问题",[258,4242,4243],{},"根因",[258,4245,3946],{},[265,4247,4248,4262,4273],{},[255,4249,4250,4253,4259],{},[270,4251,4252],{},"TOC 只有 h2",[270,4254,4255,4258],{},[45,4256,4257],{},"toc.depth"," 配置不生效",[270,4260,4261],{},"从 body AST 手动提取",[255,4263,4264,4267,4270],{},[270,4265,4266],{},"图片路径错误",[270,4268,4269],{},"SPA 路由导致相对路径解析基准错误",[270,4271,4272],{},"ProseImg 组件 + mdc.prose 配置",[255,4274,4275,4278,4281],{},[270,4276,4277],{},"content 图片 404",[270,4279,4280],{},"content 目录不暴露给浏览器",[270,4282,4283],{},"Server Route 直接读取返回",[15,4285,4286],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[15,4288,4289],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[1231,4291,4292],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPKmS, html code.shiki .sPKmS{--shiki-light:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .sCnZR, html code.shiki .sCnZR{--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .smiIW, html code.shiki .smiIW{--shiki-light:#032F62;--shiki-light-text-decoration:underline;--shiki-dark:#DBEDFF;--shiki-dark-text-decoration:underline}html pre.shiki code .szbl0, html code.shiki .szbl0{--shiki-light:#24292E;--shiki-light-text-decoration:underline;--shiki-dark:#E1E4E8;--shiki-dark-text-decoration:underline}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":81,"searchDepth":223,"depth":244,"links":4294},[4295,4296,4297,4298,4299,4300,4301,4302],{"id":3157,"depth":223,"text":3157},{"id":3170,"depth":223,"text":3170},{"id":1560,"depth":223,"text":1560},{"id":3550,"depth":223,"text":3157},{"id":3595,"depth":223,"text":3170},{"id":3630,"depth":223,"text":1560},{"id":3927,"depth":223,"text":3157},{"id":3937,"depth":223,"text":3937},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":3138,"description":4303},"posts\u002Fnuxt-content-lessons\u002Findex",[2625,4311,4312],"踩坑","Nuxt Content","vbSt6ND1z7xt7L8FxgZ8Qe3EdxV9InsRerN3E23GC7s",{"id":4315,"title":4316,"author":2734,"body":4317,"description":4581,"difficulty":2066,"draft":1260,"extension":1261,"image":4582,"meta":4583,"navigation":666,"path":4584,"pinned":1260,"published":4585,"seo":4586,"series":3130,"seriesOrder":244,"stem":4587,"tags":4588,"updated":3134,"__hash__":4591},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题",{"type":8,"value":4318,"toc":4574},[4319,4322,4325,4340,4343,4346,4349,4390,4401,4407,4429,4436,4440,4443,4446,4448,4451,4474,4485,4542,4547,4560,4562,4568,4571],[11,4320,4321],{"id":4321},"问题描述",[15,4323,4324],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[177,4326,4327,4337],{},[29,4328,4329,4330,4333,4334],{},"首次通过客户端导航（点击 ",[45,4331,4332],{},"\u003CNuxtLink>","）跳转到某个页面时，",[19,4335,4336],{},"所有链接文字都会出现下划线",[29,4338,4339],{},"手动刷新页面后，下划线消失，样式恢复正常",[15,4341,4342],{},"这个问题在开发模式和生产构建后都会出现。",[11,4344,4345],{"id":4345},"原因分析",[15,4347,4348],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[75,4350,4354],{"className":4351,"code":4352,"language":4353,"meta":81,"style":81},"language-css shiki shiki-themes github-light github-dark","a {\n  color: inherit;\n  text-decoration: inherit;\n}\n","css",[45,4355,4356,4362,4375,4386],{"__ignoreMap":81},[213,4357,4358,4360],{"class":215,"line":216},[213,4359,3097],{"class":2977},[213,4361,427],{"class":226},[213,4363,4364,4367,4369,4372],{"class":215,"line":223},[213,4365,4366],{"class":339},"  color",[213,4368,2981],{"class":226},[213,4370,4371],{"class":339},"inherit",[213,4373,4374],{"class":226},";\n",[213,4376,4377,4380,4382,4384],{"class":215,"line":244},[213,4378,4379],{"class":339},"  text-decoration",[213,4381,2981],{"class":226},[213,4383,4371],{"class":339},[213,4385,4374],{"class":226},[213,4387,4388],{"class":215,"line":360},[213,4389,445],{"class":226},[15,4391,4392,4393,4396,4397,4400],{},"正常情况下，这会让链接继承父元素的 ",[45,4394,4395],{},"text-decoration","（通常是 ",[45,4398,4399],{},"none","），从而去掉浏览器默认的下划线。",[15,4402,4403,4404,73],{},"问题出在 ",[19,4405,4406],{},"Nuxt 的客户端导航机制",[26,4408,4409,4415,4418,4423,4426],{},[29,4410,4411,4412,4414],{},"用户点击 ",[45,4413,4332],{},"，Vue Router 拦截导航",[29,4416,4417],{},"新页面组件异步加载并渲染",[29,4419,4420],{},[19,4421,4422],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[29,4424,4425],{},"此时浏览器使用默认样式（链接带下划线）",[29,4427,4428],{},"CSS 加载完成后，下划线消失",[15,4430,4431,4432,4435],{},"这就是典型的 ",[19,4433,4434],{},"FOUC（Flash of Unstyled Content）"," 问题。",[11,4437,4439],{"id":4438},"为什么刷新就正常了","为什么刷新就正常了？",[15,4441,4442],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[15,4444,4445],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[11,4447,2092],{"id":2092},[15,4449,4450],{},"尝试了多种方案：",[177,4452,4453,4459,4465,4471],{},[29,4454,4455,4458],{},[45,4456,4457],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[29,4460,4461,4464],{},[45,4462,4463],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[29,4466,4467,4470],{},[45,4468,4469],{},"features.inlineStyles: true"," — 无效",[29,4472,4473],{},"页面过渡动画 — 只是掩盖，不能根治",[15,4475,4476,4477,4480,4481,4484],{},"最终有效的方案是在 ",[45,4478,4479],{},"app.vue"," 中用 ",[45,4482,4483],{},"!important"," 强制覆盖：",[75,4486,4488],{"className":3648,"code":4487,"language":3650,"meta":81,"style":81},"\u003Cstyle>\na:not(.prose a) {\n  text-decoration-line: none !important;\n}\n\u003C\u002Fstyle>\n",[45,4489,4490,4498,4516,4530,4534],{"__ignoreMap":81},[213,4491,4492,4494,4496],{"class":215,"line":216},[213,4493,2206],{"class":226},[213,4495,1231],{"class":2977},[213,4497,1864],{"class":226},[213,4499,4500,4502,4505,4507,4510,4513],{"class":215,"line":223},[213,4501,3097],{"class":2977},[213,4503,4504],{"class":237},":not",[213,4506,739],{"class":226},[213,4508,4509],{"class":237},".prose",[213,4511,4512],{"class":2977}," a",[213,4514,4515],{"class":226},") {\n",[213,4517,4518,4521,4523,4525,4528],{"class":215,"line":244},[213,4519,4520],{"class":339},"  text-decoration-line",[213,4522,2981],{"class":226},[213,4524,4399],{"class":339},[213,4526,4527],{"class":230}," !important",[213,4529,4374],{"class":226},[213,4531,4532],{"class":215,"line":360},[213,4533,445],{"class":226},[213,4535,4536,4538,4540],{"class":215,"line":395},[213,4537,3820],{"class":226},[213,4539,1231],{"class":2977},[213,4541,1864],{"class":226},[15,4543,4544],{},[19,4545,4546],{},"关键点：",[177,4548,4549,4554],{},[29,4550,4551,4553],{},[45,4552,4483],{}," 是为了对抗 Tailwind CSS layers 的优先级",[29,4555,4556,4559],{},[45,4557,4558],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[11,4561,2439],{"id":2439},[15,4563,4564,4565,4567],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[45,4566,4483],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[15,4569,4570],{},"如果你有更好的方案，欢迎在项目仓库的 Issues 中提出！",[1231,4572,4573],{},"html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}",{"title":81,"searchDepth":223,"depth":244,"links":4575},[4576,4577,4578,4579,4580],{"id":4321,"depth":223,"text":4321},{"id":4345,"depth":223,"text":4345},{"id":4438,"depth":223,"text":4439},{"id":2092,"depth":223,"text":2092},{"id":2439,"depth":223,"text":2439},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":4316,"description":4581},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[2625,4589,4590,4311],"Tailwind CSS","CSS","WMLpTEsIoJqufwEfXyIiBgUYxcPY2XkvOwqAD4hbe6g",{"id":4593,"title":4594,"author":6,"body":4595,"description":6632,"difficulty":2066,"draft":1260,"extension":1261,"image":6633,"meta":6634,"navigation":666,"path":6635,"pinned":1260,"published":6636,"seo":6637,"series":6638,"seriesOrder":216,"stem":6639,"tags":6640,"updated":6645,"__hash__":6646},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",{"type":8,"value":4596,"toc":6613},[4597,4601,4604,4607,4611,4618,4624,4627,4630,4650,4653,4657,4681,4685,4782,4795,4799,4959,4962,5175,5179,5190,5289,5296,5300,5574,5577,5658,5662,5665,5667,5678,5682,5688,6055,6242,6245,6301,6304,6457,6460,6463,6534,6536,6601,6610],[11,4598,4600],{"id":4599},"什么是-mcp","什么是 MCP",[15,4602,4603],{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[15,4605,4606],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[11,4608,4610],{"id":4609},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[15,4612,4613,4614,4617],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[45,4615,4616],{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[15,4619,4620,4621],{},"核心思路：",[19,4622,4623],{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[11,4625,4626],{"id":4626},"需求分析",[15,4628,4629],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[26,4631,4632,4638,4644],{},[29,4633,4634,4637],{},[19,4635,4636],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[29,4639,4640,4643],{},[19,4641,4642],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[29,4645,4646,4649],{},[19,4647,4648],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[11,4651,4652],{"id":4652},"实现步骤",[113,4654,4656],{"id":4655},"_1-安装依赖","1. 安装依赖",[75,4658,4660],{"className":1291,"code":4659,"language":1293,"meta":81,"style":81},"go get github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\ngo get github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[45,4661,4662,4672],{"__ignoreMap":81},[213,4663,4664,4666,4669],{"class":215,"line":216},[213,4665,209],{"class":237},[213,4667,4668],{"class":377}," get",[213,4670,4671],{"class":377}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[213,4673,4674,4676,4678],{"class":215,"line":223},[213,4675,209],{"class":237},[213,4677,4668],{"class":377},[213,4679,4680],{"class":377}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[113,4682,4684],{"id":4683},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[75,4686,4688],{"className":207,"code":4687,"language":209,"meta":81,"style":81},"import \"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver\"\n\nmcpServer := server.NewMCPServer(\n    \"my-app\",    \u002F\u002F 服务器名称\n    \"1.0.0\",     \u002F\u002F 版本\n    server.WithLogging(),\n    server.WithRecovery(),\n    server.WithInstructions(\"这是一个 MCP 服务器示例\"),\n)\n",[45,4689,4690,4701,4705,4721,4732,4743,4754,4763,4778],{"__ignoreMap":81},[213,4691,4692,4694,4696,4699],{"class":215,"line":216},[213,4693,634],{"class":230},[213,4695,1320],{"class":377},[213,4697,4698],{"class":237},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[213,4700,648],{"class":377},[213,4702,4703],{"class":215,"line":223},[213,4704,667],{"emptyLinePlaceholder":666},[213,4706,4707,4710,4712,4715,4718],{"class":215,"line":244},[213,4708,4709],{"class":226},"mcpServer ",[213,4711,231],{"class":230},[213,4713,4714],{"class":226}," server.",[213,4716,4717],{"class":237},"NewMCPServer",[213,4719,4720],{"class":226},"(\n",[213,4722,4723,4726,4729],{"class":215,"line":360},[213,4724,4725],{"class":377},"    \"my-app\"",[213,4727,4728],{"class":226},",    ",[213,4730,4731],{"class":219},"\u002F\u002F 服务器名称\n",[213,4733,4734,4737,4740],{"class":215,"line":395},[213,4735,4736],{"class":377},"    \"1.0.0\"",[213,4738,4739],{"class":226},",     ",[213,4741,4742],{"class":219},"\u002F\u002F 版本\n",[213,4744,4745,4748,4751],{"class":215,"line":412},[213,4746,4747],{"class":226},"    server.",[213,4749,4750],{"class":237},"WithLogging",[213,4752,4753],{"class":226},"(),\n",[213,4755,4756,4758,4761],{"class":215,"line":430},[213,4757,4747],{"class":226},[213,4759,4760],{"class":237},"WithRecovery",[213,4762,4753],{"class":226},[213,4764,4765,4767,4770,4772,4775],{"class":215,"line":442},[213,4766,4747],{"class":226},[213,4768,4769],{"class":237},"WithInstructions",[213,4771,739],{"class":226},[213,4773,4774],{"class":377},"\"这是一个 MCP 服务器示例\"",[213,4776,4777],{"class":226},"),\n",[213,4779,4780],{"class":215,"line":448},[213,4781,392],{"class":226},[15,4783,4784,4786,4787,4790,4791,4794],{},[45,4785,4717],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[45,4788,4789],{},"tools\u002Flist","）和工具调用（",[45,4792,4793],{},"tools\u002Fcall","）的协议逻辑。",[113,4796,4798],{"id":4797},"_3-注册工具","3. 注册工具",[75,4800,4802],{"className":207,"code":4801,"language":209,"meta":81,"style":81},"import \"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp\"\n\n\u002F\u002F 定义工具\nlistTool := mcp.NewTool(\"list_items\",\n    mcp.WithDescription(\"列出所有项目\"),\n)\n\n\u002F\u002F 注册处理器\nmcpServer.AddTool(listTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n    \u002F\u002F 业务逻辑\n    return mcp.NewToolResultText(\"返回结果\"), nil\n})\n",[45,4803,4804,4815,4819,4824,4844,4859,4863,4867,4872,4930,4935,4955],{"__ignoreMap":81},[213,4805,4806,4808,4810,4813],{"class":215,"line":216},[213,4807,634],{"class":230},[213,4809,1320],{"class":377},[213,4811,4812],{"class":237},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[213,4814,648],{"class":377},[213,4816,4817],{"class":215,"line":223},[213,4818,667],{"emptyLinePlaceholder":666},[213,4820,4821],{"class":215,"line":244},[213,4822,4823],{"class":219},"\u002F\u002F 定义工具\n",[213,4825,4826,4829,4831,4834,4837,4839,4842],{"class":215,"line":360},[213,4827,4828],{"class":226},"listTool ",[213,4830,231],{"class":230},[213,4832,4833],{"class":226}," mcp.",[213,4835,4836],{"class":237},"NewTool",[213,4838,739],{"class":226},[213,4840,4841],{"class":377},"\"list_items\"",[213,4843,2928],{"class":226},[213,4845,4846,4849,4852,4854,4857],{"class":215,"line":395},[213,4847,4848],{"class":226},"    mcp.",[213,4850,4851],{"class":237},"WithDescription",[213,4853,739],{"class":226},[213,4855,4856],{"class":377},"\"列出所有项目\"",[213,4858,4777],{"class":226},[213,4860,4861],{"class":215,"line":412},[213,4862,392],{"class":226},[213,4864,4865],{"class":215,"line":430},[213,4866,667],{"emptyLinePlaceholder":666},[213,4868,4869],{"class":215,"line":442},[213,4870,4871],{"class":219},"\u002F\u002F 注册处理器\n",[213,4873,4874,4877,4880,4883,4885,4887,4890,4893,4895,4898,4900,4903,4906,4908,4911,4914,4916,4919,4921,4924,4926,4928],{"class":215,"line":448},[213,4875,4876],{"class":226},"mcpServer.",[213,4878,4879],{"class":237},"AddTool",[213,4881,4882],{"class":226},"(listTool, ",[213,4884,712],{"class":230},[213,4886,739],{"class":226},[213,4888,4889],{"class":865},"ctx",[213,4891,4892],{"class":237}," context",[213,4894,686],{"class":226},[213,4896,4897],{"class":237},"Context",[213,4899,381],{"class":226},[213,4901,4902],{"class":865},"req",[213,4904,4905],{"class":237}," mcp",[213,4907,686],{"class":226},[213,4909,4910],{"class":237},"CallToolRequest",[213,4912,4913],{"class":226},") (",[213,4915,343],{"class":230},[213,4917,4918],{"class":237},"mcp",[213,4920,686],{"class":226},[213,4922,4923],{"class":237},"CallToolResult",[213,4925,381],{"class":226},[213,4927,721],{"class":230},[213,4929,4515],{"class":226},[213,4931,4932],{"class":215,"line":461},[213,4933,4934],{"class":219},"    \u002F\u002F 业务逻辑\n",[213,4936,4937,4939,4941,4944,4946,4949,4952],{"class":215,"line":751},[213,4938,433],{"class":230},[213,4940,4833],{"class":226},[213,4942,4943],{"class":237},"NewToolResultText",[213,4945,739],{"class":226},[213,4947,4948],{"class":377},"\"返回结果\"",[213,4950,4951],{"class":226},"), ",[213,4953,4954],{"class":339},"nil\n",[213,4956,4957],{"class":215,"line":765},[213,4958,3815],{"class":226},[15,4960,4961],{},"带参数的工具：",[75,4963,4965],{"className":207,"code":4964,"language":209,"meta":81,"style":81},"getTool := mcp.NewTool(\"get_item\",\n    mcp.WithDescription(\"获取指定项目的详情\"),\n    mcp.WithString(\"item_id\",\n        mcp.Required(),\n        mcp.Description(\"项目 ID\"),\n    ),\n)\n\nmcpServer.AddTool(getTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n    itemID, err := req.RequireString(\"item_id\")\n    if err != nil {\n        return mcp.NewToolResultError(\"缺少 item_id 参数\"), nil\n    }\n    \u002F\u002F 查询并返回\n    return mcp.NewToolResultText(\"项目详情...\"), nil\n})\n",[45,4966,4967,4985,4998,5012,5022,5036,5041,5045,5049,5096,5115,5127,5145,5149,5154,5171],{"__ignoreMap":81},[213,4968,4969,4972,4974,4976,4978,4980,4983],{"class":215,"line":216},[213,4970,4971],{"class":226},"getTool ",[213,4973,231],{"class":230},[213,4975,4833],{"class":226},[213,4977,4836],{"class":237},[213,4979,739],{"class":226},[213,4981,4982],{"class":377},"\"get_item\"",[213,4984,2928],{"class":226},[213,4986,4987,4989,4991,4993,4996],{"class":215,"line":223},[213,4988,4848],{"class":226},[213,4990,4851],{"class":237},[213,4992,739],{"class":226},[213,4994,4995],{"class":377},"\"获取指定项目的详情\"",[213,4997,4777],{"class":226},[213,4999,5000,5002,5005,5007,5010],{"class":215,"line":244},[213,5001,4848],{"class":226},[213,5003,5004],{"class":237},"WithString",[213,5006,739],{"class":226},[213,5008,5009],{"class":377},"\"item_id\"",[213,5011,2928],{"class":226},[213,5013,5014,5017,5020],{"class":215,"line":360},[213,5015,5016],{"class":226},"        mcp.",[213,5018,5019],{"class":237},"Required",[213,5021,4753],{"class":226},[213,5023,5024,5026,5029,5031,5034],{"class":215,"line":395},[213,5025,5016],{"class":226},[213,5027,5028],{"class":237},"Description",[213,5030,739],{"class":226},[213,5032,5033],{"class":377},"\"项目 ID\"",[213,5035,4777],{"class":226},[213,5037,5038],{"class":215,"line":412},[213,5039,5040],{"class":226},"    ),\n",[213,5042,5043],{"class":215,"line":430},[213,5044,392],{"class":226},[213,5046,5047],{"class":215,"line":442},[213,5048,667],{"emptyLinePlaceholder":666},[213,5050,5051,5053,5055,5058,5060,5062,5064,5066,5068,5070,5072,5074,5076,5078,5080,5082,5084,5086,5088,5090,5092,5094],{"class":215,"line":448},[213,5052,4876],{"class":226},[213,5054,4879],{"class":237},[213,5056,5057],{"class":226},"(getTool, ",[213,5059,712],{"class":230},[213,5061,739],{"class":226},[213,5063,4889],{"class":865},[213,5065,4892],{"class":237},[213,5067,686],{"class":226},[213,5069,4897],{"class":237},[213,5071,381],{"class":226},[213,5073,4902],{"class":865},[213,5075,4905],{"class":237},[213,5077,686],{"class":226},[213,5079,4910],{"class":237},[213,5081,4913],{"class":226},[213,5083,343],{"class":230},[213,5085,4918],{"class":237},[213,5087,686],{"class":226},[213,5089,4923],{"class":237},[213,5091,381],{"class":226},[213,5093,721],{"class":230},[213,5095,4515],{"class":226},[213,5097,5098,5101,5103,5106,5109,5111,5113],{"class":215,"line":461},[213,5099,5100],{"class":226},"    itemID, err ",[213,5102,231],{"class":230},[213,5104,5105],{"class":226}," req.",[213,5107,5108],{"class":237},"RequireString",[213,5110,739],{"class":226},[213,5112,5009],{"class":377},[213,5114,392],{"class":226},[213,5116,5117,5119,5121,5123,5125],{"class":215,"line":751},[213,5118,754],{"class":230},[213,5120,418],{"class":226},[213,5122,421],{"class":230},[213,5124,424],{"class":339},[213,5126,427],{"class":226},[213,5128,5129,5131,5133,5136,5138,5141,5143],{"class":215,"line":765},[213,5130,768],{"class":230},[213,5132,4833],{"class":226},[213,5134,5135],{"class":237},"NewToolResultError",[213,5137,739],{"class":226},[213,5139,5140],{"class":377},"\"缺少 item_id 参数\"",[213,5142,4951],{"class":226},[213,5144,4954],{"class":339},[213,5146,5147],{"class":215,"line":774},[213,5148,777],{"class":226},[213,5150,5151],{"class":215,"line":780},[213,5152,5153],{"class":219},"    \u002F\u002F 查询并返回\n",[213,5155,5156,5158,5160,5162,5164,5167,5169],{"class":215,"line":797},[213,5157,433],{"class":230},[213,5159,4833],{"class":226},[213,5161,4943],{"class":237},[213,5163,739],{"class":226},[213,5165,5166],{"class":377},"\"项目详情...\"",[213,5168,4951],{"class":226},[213,5170,4954],{"class":339},[213,5172,5173],{"class":215,"line":810},[213,5174,3815],{"class":226},[113,5176,5178],{"id":5177},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[15,5180,5181,5182,5185,5186,5189],{},"关键一步：用 ",[45,5183,5184],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[45,5187,5188],{},"gin.WrapH"," 适配 Gin。",[75,5191,5193],{"className":207,"code":5192,"language":209,"meta":81,"style":81},"import \"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver\"\n\nmcpHTTPServer := server.NewStreamableHTTPServer(mcpServer,\n    server.WithEndpointPath(\"\u002Fmcp\"),\n    server.WithHeartbeatInterval(30*time.Second),\n)\n\n\u002F\u002F 挂载到 Gin\nr.Any(\"\u002Fmcp\", gin.WrapH(mcpHTTPServer))\n",[45,5194,5195,5205,5209,5224,5238,5255,5259,5263,5268],{"__ignoreMap":81},[213,5196,5197,5199,5201,5203],{"class":215,"line":216},[213,5198,634],{"class":230},[213,5200,1320],{"class":377},[213,5202,4698],{"class":237},[213,5204,648],{"class":377},[213,5206,5207],{"class":215,"line":223},[213,5208,667],{"emptyLinePlaceholder":666},[213,5210,5211,5214,5216,5218,5221],{"class":215,"line":244},[213,5212,5213],{"class":226},"mcpHTTPServer ",[213,5215,231],{"class":230},[213,5217,4714],{"class":226},[213,5219,5220],{"class":237},"NewStreamableHTTPServer",[213,5222,5223],{"class":226},"(mcpServer,\n",[213,5225,5226,5228,5231,5233,5236],{"class":215,"line":360},[213,5227,4747],{"class":226},[213,5229,5230],{"class":237},"WithEndpointPath",[213,5232,739],{"class":226},[213,5234,5235],{"class":377},"\"\u002Fmcp\"",[213,5237,4777],{"class":226},[213,5239,5240,5242,5245,5247,5250,5252],{"class":215,"line":395},[213,5241,4747],{"class":226},[213,5243,5244],{"class":237},"WithHeartbeatInterval",[213,5246,739],{"class":226},[213,5248,5249],{"class":339},"30",[213,5251,343],{"class":230},[213,5253,5254],{"class":226},"time.Second),\n",[213,5256,5257],{"class":215,"line":412},[213,5258,392],{"class":226},[213,5260,5261],{"class":215,"line":430},[213,5262,667],{"emptyLinePlaceholder":666},[213,5264,5265],{"class":215,"line":442},[213,5266,5267],{"class":219},"\u002F\u002F 挂载到 Gin\n",[213,5269,5270,5273,5276,5278,5280,5283,5286],{"class":215,"line":448},[213,5271,5272],{"class":226},"r.",[213,5274,5275],{"class":237},"Any",[213,5277,739],{"class":226},[213,5279,5235],{"class":377},[213,5281,5282],{"class":226},", gin.",[213,5284,5285],{"class":237},"WrapH",[213,5287,5288],{"class":226},"(mcpHTTPServer))\n",[15,5290,5291,5292,5295],{},"就这样，",[45,5293,5294],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[113,5297,5299],{"id":5298},"_5-完整的服务启动代码","5. 完整的服务启动代码",[75,5301,5303],{"className":207,"code":5302,"language":209,"meta":81,"style":81},"func main() {\n    r := gin.Default()\n\n    \u002F\u002F 创建 MCP 服务器\n    mcpServer := server.NewMCPServer(\"my-app\", \"1.0.0\",\n        server.WithLogging(),\n        server.WithRecovery(),\n    )\n\n    \u002F\u002F 注册工具\n    registerTools(mcpServer)\n\n    \u002F\u002F 挂载到 Gin\n    mcpHTTP := server.NewStreamableHTTPServer(mcpServer,\n        server.WithEndpointPath(\"\u002Fmcp\"),\n        server.WithHeartbeatInterval(30*time.Second),\n    )\n    r.Any(\"\u002Fmcp\", gin.WrapH(mcpHTTP))\n\n    \u002F\u002F 其他业务路由\n    r.GET(\"\u002Fapi\u002Fhealth\", func(c *gin.Context) {\n        c.JSON(200, gin.H{\"status\": \"ok\"})\n    })\n\n    r.Run(\":8080\")\n}\n",[45,5304,5305,5314,5329,5333,5338,5361,5370,5378,5383,5387,5392,5400,5404,5409,5422,5434,5448,5452,5470,5474,5479,5512,5547,5552,5556,5570],{"__ignoreMap":81},[213,5306,5307,5309,5312],{"class":215,"line":216},[213,5308,712],{"class":230},[213,5310,5311],{"class":237}," main",[213,5313,2566],{"class":226},[213,5315,5316,5319,5321,5324,5327],{"class":215,"line":223},[213,5317,5318],{"class":226},"    r ",[213,5320,231],{"class":230},[213,5322,5323],{"class":226}," gin.",[213,5325,5326],{"class":237},"Default",[213,5328,357],{"class":226},[213,5330,5331],{"class":215,"line":244},[213,5332,667],{"emptyLinePlaceholder":666},[213,5334,5335],{"class":215,"line":360},[213,5336,5337],{"class":219},"    \u002F\u002F 创建 MCP 服务器\n",[213,5339,5340,5343,5345,5347,5349,5351,5354,5356,5359],{"class":215,"line":395},[213,5341,5342],{"class":226},"    mcpServer ",[213,5344,231],{"class":230},[213,5346,4714],{"class":226},[213,5348,4717],{"class":237},[213,5350,739],{"class":226},[213,5352,5353],{"class":377},"\"my-app\"",[213,5355,381],{"class":226},[213,5357,5358],{"class":377},"\"1.0.0\"",[213,5360,2928],{"class":226},[213,5362,5363,5366,5368],{"class":215,"line":412},[213,5364,5365],{"class":226},"        server.",[213,5367,4750],{"class":237},[213,5369,4753],{"class":226},[213,5371,5372,5374,5376],{"class":215,"line":430},[213,5373,5365],{"class":226},[213,5375,4760],{"class":237},[213,5377,4753],{"class":226},[213,5379,5380],{"class":215,"line":442},[213,5381,5382],{"class":226},"    )\n",[213,5384,5385],{"class":215,"line":448},[213,5386,667],{"emptyLinePlaceholder":666},[213,5388,5389],{"class":215,"line":461},[213,5390,5391],{"class":219},"    \u002F\u002F 注册工具\n",[213,5393,5394,5397],{"class":215,"line":751},[213,5395,5396],{"class":237},"    registerTools",[213,5398,5399],{"class":226},"(mcpServer)\n",[213,5401,5402],{"class":215,"line":765},[213,5403,667],{"emptyLinePlaceholder":666},[213,5405,5406],{"class":215,"line":774},[213,5407,5408],{"class":219},"    \u002F\u002F 挂载到 Gin\n",[213,5410,5411,5414,5416,5418,5420],{"class":215,"line":780},[213,5412,5413],{"class":226},"    mcpHTTP ",[213,5415,231],{"class":230},[213,5417,4714],{"class":226},[213,5419,5220],{"class":237},[213,5421,5223],{"class":226},[213,5423,5424,5426,5428,5430,5432],{"class":215,"line":797},[213,5425,5365],{"class":226},[213,5427,5230],{"class":237},[213,5429,739],{"class":226},[213,5431,5235],{"class":377},[213,5433,4777],{"class":226},[213,5435,5436,5438,5440,5442,5444,5446],{"class":215,"line":810},[213,5437,5365],{"class":226},[213,5439,5244],{"class":237},[213,5441,739],{"class":226},[213,5443,5249],{"class":339},[213,5445,343],{"class":230},[213,5447,5254],{"class":226},[213,5449,5450],{"class":215,"line":817},[213,5451,5382],{"class":226},[213,5453,5454,5457,5459,5461,5463,5465,5467],{"class":215,"line":822},[213,5455,5456],{"class":226},"    r.",[213,5458,5275],{"class":237},[213,5460,739],{"class":226},[213,5462,5235],{"class":377},[213,5464,5282],{"class":226},[213,5466,5285],{"class":237},[213,5468,5469],{"class":226},"(mcpHTTP))\n",[213,5471,5472],{"class":215,"line":834},[213,5473,667],{"emptyLinePlaceholder":666},[213,5475,5476],{"class":215,"line":842},[213,5477,5478],{"class":219},"    \u002F\u002F 其他业务路由\n",[213,5480,5481,5483,5486,5488,5491,5493,5495,5497,5500,5503,5506,5508,5510],{"class":215,"line":1714},[213,5482,5456],{"class":226},[213,5484,5485],{"class":237},"GET",[213,5487,739],{"class":226},[213,5489,5490],{"class":377},"\"\u002Fapi\u002Fhealth\"",[213,5492,381],{"class":226},[213,5494,712],{"class":230},[213,5496,739],{"class":226},[213,5498,5499],{"class":865},"c",[213,5501,5502],{"class":230}," *",[213,5504,5505],{"class":237},"gin",[213,5507,686],{"class":226},[213,5509,4897],{"class":237},[213,5511,4515],{"class":226},[213,5513,5514,5517,5520,5522,5525,5527,5529,5531,5534,5537,5540,5542,5545],{"class":215,"line":1719},[213,5515,5516],{"class":226},"        c.",[213,5518,5519],{"class":237},"JSON",[213,5521,739],{"class":226},[213,5523,5524],{"class":339},"200",[213,5526,381],{"class":226},[213,5528,5505],{"class":237},[213,5530,686],{"class":226},[213,5532,5533],{"class":237},"H",[213,5535,5536],{"class":226},"{",[213,5538,5539],{"class":377},"\"status\"",[213,5541,2981],{"class":226},[213,5543,5544],{"class":377},"\"ok\"",[213,5546,3815],{"class":226},[213,5548,5549],{"class":215,"line":2350},[213,5550,5551],{"class":226},"    })\n",[213,5553,5554],{"class":215,"line":2364},[213,5555,667],{"emptyLinePlaceholder":666},[213,5557,5558,5560,5563,5565,5568],{"class":215,"line":2371},[213,5559,5456],{"class":226},[213,5561,5562],{"class":237},"Run",[213,5564,739],{"class":226},[213,5566,5567],{"class":377},"\":8080\"",[213,5569,392],{"class":226},[213,5571,5572],{"class":215,"line":2376},[213,5573,445],{"class":226},[15,5575,5576],{},"此时启动服务，用 curl 测试：",[75,5578,5580],{"className":1291,"code":5579,"language":1293,"meta":81,"style":81},"# 发现工具列表\ncurl -X POST http:\u002F\u002Flocalhost:8080\u002Fmcp \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n\n# 调用工具\ncurl -X POST http:\u002F\u002Flocalhost:8080\u002Fmcp \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[45,5581,5582,5587,5604,5614,5622,5626,5631,5643,5651],{"__ignoreMap":81},[213,5583,5584],{"class":215,"line":216},[213,5585,5586],{"class":219},"# 发现工具列表\n",[213,5588,5589,5592,5595,5598,5601],{"class":215,"line":223},[213,5590,5591],{"class":237},"curl",[213,5593,5594],{"class":339}," -X",[213,5596,5597],{"class":377}," POST",[213,5599,5600],{"class":377}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[213,5602,5603],{"class":339}," \\\n",[213,5605,5606,5609,5612],{"class":215,"line":244},[213,5607,5608],{"class":339},"  -H",[213,5610,5611],{"class":377}," \"Content-Type: application\u002Fjson\"",[213,5613,5603],{"class":339},[213,5615,5616,5619],{"class":215,"line":360},[213,5617,5618],{"class":339},"  -d",[213,5620,5621],{"class":377}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[213,5623,5624],{"class":215,"line":395},[213,5625,667],{"emptyLinePlaceholder":666},[213,5627,5628],{"class":215,"line":412},[213,5629,5630],{"class":219},"# 调用工具\n",[213,5632,5633,5635,5637,5639,5641],{"class":215,"line":430},[213,5634,5591],{"class":237},[213,5636,5594],{"class":339},[213,5638,5597],{"class":377},[213,5640,5600],{"class":377},[213,5642,5603],{"class":339},[213,5644,5645,5647,5649],{"class":215,"line":442},[213,5646,5608],{"class":339},[213,5648,5611],{"class":377},[213,5650,5603],{"class":339},[213,5652,5653,5655],{"class":215,"line":448},[213,5654,5618],{"class":339},[213,5656,5657],{"class":377}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[11,5659,5661],{"id":5660},"拓展接入鉴权","拓展：接入鉴权",[15,5663,5664],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[113,5666,4240],{"id":4240},[15,5668,5669,5670,5673,5674,5677],{},"MCP 的工具处理器签名为 ",[45,5671,5672],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[45,5675,5676],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[113,5679,5681],{"id":5680},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[15,5683,5684,5685,59],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[45,5686,5687],{},"request.Context",[75,5689,5691],{"className":207,"code":5690,"language":209,"meta":81,"style":81},"\u002F\u002F Gin 鉴权中间件\nfunc AuthRequired(authService *AuthService) gin.HandlerFunc {\n    return func(c *gin.Context) {\n        authHeader := c.GetHeader(\"Authorization\")\n        if authHeader == \"\" {\n            c.AbortWithStatusJSON(401, gin.H{\"message\": \"缺少认证令牌\"})\n            return\n        }\n\n        parts := strings.SplitN(authHeader, \" \", 2)\n        if len(parts) != 2 || !strings.EqualFold(parts[0], \"Bearer\") {\n            c.AbortWithStatusJSON(401, gin.H{\"message\": \"格式错误，请使用 Bearer \u003Ctoken>\"})\n            return\n        }\n\n        user, err := authService.GetUserByToken(parts[1])\n        if err != nil {\n            c.AbortWithStatusJSON(401, gin.H{\"message\": \"无效的认证令牌\"})\n            return\n        }\n\n        c.Set(\"userID\", user.ID)\n        c.Set(\"username\", user.Username)\n        c.Next()\n    }\n}\n",[45,5692,5693,5698,5726,5747,5767,5783,5816,5821,5826,5830,5855,5895,5924,5928,5932,5936,5956,5968,5997,6001,6005,6009,6024,6038,6047,6051],{"__ignoreMap":81},[213,5694,5695],{"class":215,"line":216},[213,5696,5697],{"class":219},"\u002F\u002F Gin 鉴权中间件\n",[213,5699,5700,5702,5705,5707,5710,5712,5715,5717,5719,5721,5724],{"class":215,"line":223},[213,5701,712],{"class":230},[213,5703,5704],{"class":237}," AuthRequired",[213,5706,739],{"class":226},[213,5708,5709],{"class":865},"authService",[213,5711,5502],{"class":230},[213,5713,5714],{"class":237},"AuthService",[213,5716,745],{"class":226},[213,5718,5505],{"class":237},[213,5720,686],{"class":226},[213,5722,5723],{"class":237},"HandlerFunc",[213,5725,427],{"class":226},[213,5727,5728,5730,5733,5735,5737,5739,5741,5743,5745],{"class":215,"line":244},[213,5729,433],{"class":230},[213,5731,5732],{"class":230}," func",[213,5734,739],{"class":226},[213,5736,5499],{"class":865},[213,5738,5502],{"class":230},[213,5740,5505],{"class":237},[213,5742,686],{"class":226},[213,5744,4897],{"class":237},[213,5746,4515],{"class":226},[213,5748,5749,5752,5754,5757,5760,5762,5765],{"class":215,"line":360},[213,5750,5751],{"class":226},"        authHeader ",[213,5753,231],{"class":230},[213,5755,5756],{"class":226}," c.",[213,5758,5759],{"class":237},"GetHeader",[213,5761,739],{"class":226},[213,5763,5764],{"class":377},"\"Authorization\"",[213,5766,392],{"class":226},[213,5768,5769,5772,5775,5778,5781],{"class":215,"line":395},[213,5770,5771],{"class":230},"        if",[213,5773,5774],{"class":226}," authHeader ",[213,5776,5777],{"class":230},"==",[213,5779,5780],{"class":377}," \"\"",[213,5782,427],{"class":226},[213,5784,5785,5788,5791,5793,5796,5798,5800,5802,5804,5806,5809,5811,5814],{"class":215,"line":412},[213,5786,5787],{"class":226},"            c.",[213,5789,5790],{"class":237},"AbortWithStatusJSON",[213,5792,739],{"class":226},[213,5794,5795],{"class":339},"401",[213,5797,381],{"class":226},[213,5799,5505],{"class":237},[213,5801,686],{"class":226},[213,5803,5533],{"class":237},[213,5805,5536],{"class":226},[213,5807,5808],{"class":377},"\"message\"",[213,5810,2981],{"class":226},[213,5812,5813],{"class":377},"\"缺少认证令牌\"",[213,5815,3815],{"class":226},[213,5817,5818],{"class":215,"line":430},[213,5819,5820],{"class":230},"            return\n",[213,5822,5823],{"class":215,"line":442},[213,5824,5825],{"class":226},"        }\n",[213,5827,5828],{"class":215,"line":448},[213,5829,667],{"emptyLinePlaceholder":666},[213,5831,5832,5835,5837,5840,5843,5846,5849,5851,5853],{"class":215,"line":461},[213,5833,5834],{"class":226},"        parts ",[213,5836,231],{"class":230},[213,5838,5839],{"class":226}," strings.",[213,5841,5842],{"class":237},"SplitN",[213,5844,5845],{"class":226},"(authHeader, ",[213,5847,5848],{"class":377},"\" \"",[213,5850,381],{"class":226},[213,5852,3456],{"class":339},[213,5854,392],{"class":226},[213,5856,5857,5859,5862,5865,5867,5870,5873,5876,5879,5882,5885,5887,5890,5893],{"class":215,"line":751},[213,5858,5771],{"class":230},[213,5860,5861],{"class":237}," len",[213,5863,5864],{"class":226},"(parts) ",[213,5866,421],{"class":230},[213,5868,5869],{"class":339}," 2",[213,5871,5872],{"class":230}," ||",[213,5874,5875],{"class":230}," !",[213,5877,5878],{"class":226},"strings.",[213,5880,5881],{"class":237},"EqualFold",[213,5883,5884],{"class":226},"(parts[",[213,5886,3359],{"class":339},[213,5888,5889],{"class":226},"], ",[213,5891,5892],{"class":377},"\"Bearer\"",[213,5894,4515],{"class":226},[213,5896,5897,5899,5901,5903,5905,5907,5909,5911,5913,5915,5917,5919,5922],{"class":215,"line":765},[213,5898,5787],{"class":226},[213,5900,5790],{"class":237},[213,5902,739],{"class":226},[213,5904,5795],{"class":339},[213,5906,381],{"class":226},[213,5908,5505],{"class":237},[213,5910,686],{"class":226},[213,5912,5533],{"class":237},[213,5914,5536],{"class":226},[213,5916,5808],{"class":377},[213,5918,2981],{"class":226},[213,5920,5921],{"class":377},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[213,5923,3815],{"class":226},[213,5925,5926],{"class":215,"line":774},[213,5927,5820],{"class":230},[213,5929,5930],{"class":215,"line":780},[213,5931,5825],{"class":226},[213,5933,5934],{"class":215,"line":797},[213,5935,667],{"emptyLinePlaceholder":666},[213,5937,5938,5941,5943,5946,5949,5951,5953],{"class":215,"line":810},[213,5939,5940],{"class":226},"        user, err ",[213,5942,231],{"class":230},[213,5944,5945],{"class":226}," authService.",[213,5947,5948],{"class":237},"GetUserByToken",[213,5950,5884],{"class":226},[213,5952,1404],{"class":339},[213,5954,5955],{"class":226},"])\n",[213,5957,5958,5960,5962,5964,5966],{"class":215,"line":817},[213,5959,5771],{"class":230},[213,5961,418],{"class":226},[213,5963,421],{"class":230},[213,5965,424],{"class":339},[213,5967,427],{"class":226},[213,5969,5970,5972,5974,5976,5978,5980,5982,5984,5986,5988,5990,5992,5995],{"class":215,"line":822},[213,5971,5787],{"class":226},[213,5973,5790],{"class":237},[213,5975,739],{"class":226},[213,5977,5795],{"class":339},[213,5979,381],{"class":226},[213,5981,5505],{"class":237},[213,5983,686],{"class":226},[213,5985,5533],{"class":237},[213,5987,5536],{"class":226},[213,5989,5808],{"class":377},[213,5991,2981],{"class":226},[213,5993,5994],{"class":377},"\"无效的认证令牌\"",[213,5996,3815],{"class":226},[213,5998,5999],{"class":215,"line":834},[213,6000,5820],{"class":230},[213,6002,6003],{"class":215,"line":842},[213,6004,5825],{"class":226},[213,6006,6007],{"class":215,"line":1714},[213,6008,667],{"emptyLinePlaceholder":666},[213,6010,6011,6013,6016,6018,6021],{"class":215,"line":1719},[213,6012,5516],{"class":226},[213,6014,6015],{"class":237},"Set",[213,6017,739],{"class":226},[213,6019,6020],{"class":377},"\"userID\"",[213,6022,6023],{"class":226},", user.ID)\n",[213,6025,6026,6028,6030,6032,6035],{"class":215,"line":2350},[213,6027,5516],{"class":226},[213,6029,6015],{"class":237},[213,6031,739],{"class":226},[213,6033,6034],{"class":377},"\"username\"",[213,6036,6037],{"class":226},", user.Username)\n",[213,6039,6040,6042,6045],{"class":215,"line":2364},[213,6041,5516],{"class":226},[213,6043,6044],{"class":237},"Next",[213,6046,357],{"class":226},[213,6048,6049],{"class":215,"line":2371},[213,6050,777],{"class":226},[213,6052,6053],{"class":215,"line":2376},[213,6054,445],{"class":226},[75,6056,6058],{"className":207,"code":6057,"language":209,"meta":81,"style":81},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\ntype ctxKey string\nconst (\n    ctxUserID   ctxKey = \"userID\"\n    ctxUsername ctxKey = \"username\"\n)\n\nfunc WithUserContext() gin.HandlerFunc {\n    return func(c *gin.Context) {\n        ctx := c.Request.Context()\n        ctx = context.WithValue(ctx, ctxUserID, c.GetUint(\"userID\"))\n        ctx = context.WithValue(ctx, ctxUsername, c.GetString(\"username\"))\n        c.Request = c.Request.WithContext(ctx)\n        c.Next()\n    }\n}\n",[45,6059,6060,6065,6076,6082,6095,6107,6111,6115,6132,6152,6166,6189,6211,6226,6234,6238],{"__ignoreMap":81},[213,6061,6062],{"class":215,"line":216},[213,6063,6064],{"class":219},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[213,6066,6067,6070,6073],{"class":215,"line":223},[213,6068,6069],{"class":230},"type",[213,6071,6072],{"class":237}," ctxKey",[213,6074,6075],{"class":230}," string\n",[213,6077,6078,6080],{"class":215,"line":244},[213,6079,3503],{"class":230},[213,6081,637],{"class":226},[213,6083,6084,6087,6090,6092],{"class":215,"line":360},[213,6085,6086],{"class":339},"    ctxUserID",[213,6088,6089],{"class":237},"   ctxKey",[213,6091,2915],{"class":230},[213,6093,6094],{"class":377}," \"userID\"\n",[213,6096,6097,6100,6102,6104],{"class":215,"line":395},[213,6098,6099],{"class":339},"    ctxUsername",[213,6101,6072],{"class":237},[213,6103,2915],{"class":230},[213,6105,6106],{"class":377}," \"username\"\n",[213,6108,6109],{"class":215,"line":412},[213,6110,392],{"class":226},[213,6112,6113],{"class":215,"line":430},[213,6114,667],{"emptyLinePlaceholder":666},[213,6116,6117,6119,6122,6124,6126,6128,6130],{"class":215,"line":442},[213,6118,712],{"class":230},[213,6120,6121],{"class":237}," WithUserContext",[213,6123,718],{"class":226},[213,6125,5505],{"class":237},[213,6127,686],{"class":226},[213,6129,5723],{"class":237},[213,6131,427],{"class":226},[213,6133,6134,6136,6138,6140,6142,6144,6146,6148,6150],{"class":215,"line":448},[213,6135,433],{"class":230},[213,6137,5732],{"class":230},[213,6139,739],{"class":226},[213,6141,5499],{"class":865},[213,6143,5502],{"class":230},[213,6145,5505],{"class":237},[213,6147,686],{"class":226},[213,6149,4897],{"class":237},[213,6151,4515],{"class":226},[213,6153,6154,6157,6159,6162,6164],{"class":215,"line":461},[213,6155,6156],{"class":226},"        ctx ",[213,6158,231],{"class":230},[213,6160,6161],{"class":226}," c.Request.",[213,6163,4897],{"class":237},[213,6165,357],{"class":226},[213,6167,6168,6170,6172,6174,6177,6180,6183,6185,6187],{"class":215,"line":751},[213,6169,6156],{"class":226},[213,6171,1386],{"class":230},[213,6173,324],{"class":226},[213,6175,6176],{"class":237},"WithValue",[213,6178,6179],{"class":226},"(ctx, ctxUserID, c.",[213,6181,6182],{"class":237},"GetUint",[213,6184,739],{"class":226},[213,6186,6020],{"class":377},[213,6188,3433],{"class":226},[213,6190,6191,6193,6195,6197,6199,6202,6205,6207,6209],{"class":215,"line":765},[213,6192,6156],{"class":226},[213,6194,1386],{"class":230},[213,6196,324],{"class":226},[213,6198,6176],{"class":237},[213,6200,6201],{"class":226},"(ctx, ctxUsername, c.",[213,6203,6204],{"class":237},"GetString",[213,6206,739],{"class":226},[213,6208,6034],{"class":377},[213,6210,3433],{"class":226},[213,6212,6213,6216,6218,6220,6223],{"class":215,"line":774},[213,6214,6215],{"class":226},"        c.Request ",[213,6217,1386],{"class":230},[213,6219,6161],{"class":226},[213,6221,6222],{"class":237},"WithContext",[213,6224,6225],{"class":226},"(ctx)\n",[213,6227,6228,6230,6232],{"class":215,"line":780},[213,6229,5516],{"class":226},[213,6231,6044],{"class":237},[213,6233,357],{"class":226},[213,6235,6236],{"class":215,"line":797},[213,6237,777],{"class":226},[213,6239,6240],{"class":215,"line":810},[213,6241,445],{"class":226},[113,6243,6244],{"id":6244},"组合使用",[75,6246,6248],{"className":207,"code":6247,"language":209,"meta":81,"style":81},"r.Any(\"\u002Fmcp\",\n    AuthRequired(authService),  \u002F\u002F 1. 验证 Token\n    WithUserContext(),           \u002F\u002F 2. 桥接用户信息到 request.Context\n    gin.WrapH(mcpHTTP),         \u002F\u002F 3. MCP 处理\n)\n",[45,6249,6250,6262,6273,6284,6297],{"__ignoreMap":81},[213,6251,6252,6254,6256,6258,6260],{"class":215,"line":216},[213,6253,5272],{"class":226},[213,6255,5275],{"class":237},[213,6257,739],{"class":226},[213,6259,5235],{"class":377},[213,6261,2928],{"class":226},[213,6263,6264,6267,6270],{"class":215,"line":223},[213,6265,6266],{"class":237},"    AuthRequired",[213,6268,6269],{"class":226},"(authService),  ",[213,6271,6272],{"class":219},"\u002F\u002F 1. 验证 Token\n",[213,6274,6275,6278,6281],{"class":215,"line":244},[213,6276,6277],{"class":237},"    WithUserContext",[213,6279,6280],{"class":226},"(),           ",[213,6282,6283],{"class":219},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[213,6285,6286,6289,6291,6294],{"class":215,"line":360},[213,6287,6288],{"class":226},"    gin.",[213,6290,5285],{"class":237},[213,6292,6293],{"class":226},"(mcpHTTP),         ",[213,6295,6296],{"class":219},"\u002F\u002F 3. MCP 处理\n",[213,6298,6299],{"class":215,"line":395},[213,6300,392],{"class":226},[15,6302,6303],{},"在工具处理器里就能拿到用户信息了：",[75,6305,6307],{"className":207,"code":6306,"language":209,"meta":81,"style":81},"func (h *Handler) handleListItems(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n    userID, ok := ctx.Value(ctxUserID).(uint)\n    if !ok {\n        return mcp.NewToolResultError(\"未认证\"), nil\n    }\n\n    items, _ := h.service.ListByUser(userID)\n    \u002F\u002F ...\n    return mcp.NewToolResultText(result), nil\n}\n",[45,6308,6309,6364,6385,6394,6411,6415,6419,6435,6440,6453],{"__ignoreMap":81},[213,6310,6311,6313,6315,6318,6320,6323,6325,6328,6330,6332,6334,6336,6338,6340,6342,6344,6346,6348,6350,6352,6354,6356,6358,6360,6362],{"class":215,"line":216},[213,6312,712],{"class":230},[213,6314,3314],{"class":226},[213,6316,6317],{"class":865},"h ",[213,6319,343],{"class":230},[213,6321,6322],{"class":237},"Handler",[213,6324,745],{"class":226},[213,6326,6327],{"class":237},"handleListItems",[213,6329,739],{"class":226},[213,6331,4889],{"class":865},[213,6333,4892],{"class":237},[213,6335,686],{"class":226},[213,6337,4897],{"class":237},[213,6339,381],{"class":226},[213,6341,4902],{"class":865},[213,6343,4905],{"class":237},[213,6345,686],{"class":226},[213,6347,4910],{"class":237},[213,6349,4913],{"class":226},[213,6351,343],{"class":230},[213,6353,4918],{"class":237},[213,6355,686],{"class":226},[213,6357,4923],{"class":237},[213,6359,381],{"class":226},[213,6361,721],{"class":230},[213,6363,4515],{"class":226},[213,6365,6366,6369,6371,6374,6377,6380,6383],{"class":215,"line":223},[213,6367,6368],{"class":226},"    userID, ok ",[213,6370,231],{"class":230},[213,6372,6373],{"class":226}," ctx.",[213,6375,6376],{"class":237},"Value",[213,6378,6379],{"class":226},"(ctxUserID).(",[213,6381,6382],{"class":230},"uint",[213,6384,392],{"class":226},[213,6386,6387,6389,6391],{"class":215,"line":244},[213,6388,754],{"class":230},[213,6390,5875],{"class":230},[213,6392,6393],{"class":226},"ok {\n",[213,6395,6396,6398,6400,6402,6404,6407,6409],{"class":215,"line":360},[213,6397,768],{"class":230},[213,6399,4833],{"class":226},[213,6401,5135],{"class":237},[213,6403,739],{"class":226},[213,6405,6406],{"class":377},"\"未认证\"",[213,6408,4951],{"class":226},[213,6410,4954],{"class":339},[213,6412,6413],{"class":215,"line":395},[213,6414,777],{"class":226},[213,6416,6417],{"class":215,"line":412},[213,6418,667],{"emptyLinePlaceholder":666},[213,6420,6421,6424,6426,6429,6432],{"class":215,"line":430},[213,6422,6423],{"class":226},"    items, _ ",[213,6425,231],{"class":230},[213,6427,6428],{"class":226}," h.service.",[213,6430,6431],{"class":237},"ListByUser",[213,6433,6434],{"class":226},"(userID)\n",[213,6436,6437],{"class":215,"line":442},[213,6438,6439],{"class":219},"    \u002F\u002F ...\n",[213,6441,6442,6444,6446,6448,6451],{"class":215,"line":448},[213,6443,433],{"class":230},[213,6445,4833],{"class":226},[213,6447,4943],{"class":237},[213,6449,6450],{"class":226},"(result), ",[213,6452,4954],{"class":339},[213,6454,6455],{"class":215,"line":461},[213,6456,445],{"class":226},[113,6458,6459],{"id":6459},"客户端配置",[15,6461,6462],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[75,6464,6468],{"className":6465,"code":6466,"language":6467,"meta":81,"style":81},"language-json shiki shiki-themes github-light github-dark","{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"url\": \"http:\u002F\u002Flocalhost:8080\u002Fmcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer your-token-here\"\n      }\n    }\n  }\n}\n","json",[45,6469,6470,6475,6482,6488,6500,6507,6517,6522,6526,6530],{"__ignoreMap":81},[213,6471,6472],{"class":215,"line":216},[213,6473,6474],{"class":226},"{\n",[213,6476,6477,6480],{"class":215,"line":223},[213,6478,6479],{"class":339},"  \"mcpServers\"",[213,6481,3195],{"class":226},[213,6483,6484,6486],{"class":215,"line":244},[213,6485,4725],{"class":339},[213,6487,3195],{"class":226},[213,6489,6490,6493,6495,6498],{"class":215,"line":360},[213,6491,6492],{"class":339},"      \"url\"",[213,6494,2981],{"class":226},[213,6496,6497],{"class":377},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[213,6499,2928],{"class":226},[213,6501,6502,6505],{"class":215,"line":395},[213,6503,6504],{"class":339},"      \"headers\"",[213,6506,3195],{"class":226},[213,6508,6509,6512,6514],{"class":215,"line":412},[213,6510,6511],{"class":339},"        \"Authorization\"",[213,6513,2981],{"class":226},[213,6515,6516],{"class":377},"\"Bearer your-token-here\"\n",[213,6518,6519],{"class":215,"line":430},[213,6520,6521],{"class":226},"      }\n",[213,6523,6524],{"class":215,"line":442},[213,6525,777],{"class":226},[213,6527,6528],{"class":215,"line":448},[213,6529,3239],{"class":226},[213,6531,6532],{"class":215,"line":461},[213,6533,445],{"class":226},[11,6535,2439],{"id":2439},[249,6537,6538,6548],{},[252,6539,6540],{},[255,6541,6542,6545],{},[258,6543,6544],{},"步骤",[258,6546,6547],{},"说明",[265,6549,6550,6559,6570,6579,6588],{},[255,6551,6552,6556],{},[270,6553,6554],{},[45,6555,4717],{},[270,6557,6558],{},"创建 MCP 协议实例",[255,6560,6561,6567],{},[270,6562,6563,2456,6565],{},[45,6564,4836],{},[45,6566,4879],{},[270,6568,6569],{},"定义并注册工具",[255,6571,6572,6576],{},[270,6573,6574],{},[45,6575,5220],{},[270,6577,6578],{},"转为 HTTP Handler",[255,6580,6581,6585],{},[270,6582,6583],{},[45,6584,5188],{},[270,6586,6587],{},"挂载到 Gin 路由",[255,6589,6590,6598],{},[270,6591,6592,2456,6595],{},[45,6593,6594],{},"AuthRequired",[45,6596,6597],{},"WithUserContext",[270,6599,6600],{},"鉴权 + 用户信息桥接",[15,6602,6603,6604,6607,6608,59],{},"核心就一句话：",[19,6605,6606],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[45,6609,5687],{},[1231,6611,6612],{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}",{"title":81,"searchDepth":223,"depth":244,"links":6614},[6615,6616,6617,6618,6625,6631],{"id":4599,"depth":223,"text":4600},{"id":4609,"depth":223,"text":4610},{"id":4626,"depth":223,"text":4626},{"id":4652,"depth":223,"text":4652,"children":6619},[6620,6621,6622,6623,6624],{"id":4655,"depth":244,"text":4656},{"id":4683,"depth":244,"text":4684},{"id":4797,"depth":244,"text":4798},{"id":5177,"depth":244,"text":5178},{"id":5298,"depth":244,"text":5299},{"id":5660,"depth":223,"text":5661,"children":6626},[6627,6628,6629,6630],{"id":4240,"depth":244,"text":4240},{"id":5680,"depth":244,"text":5681},{"id":6244,"depth":244,"text":6244},{"id":6459,"depth":244,"text":6459},{"id":2439,"depth":223,"text":2439},"介绍如何使用 mark3labs\u002Fmcp-go 与 Gin 框架集成，搭建一个带鉴权的 MCP（Model Context Protocol）服务器，让 AI Agent 能直接调用你的后端服务。","\u002Fposts\u002Fgo-mcp-gin\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-mcp-gin","2026-06-01T00:50:00",{"title":4594,"description":6632},"Go 工程实践","posts\u002Fgo-mcp-gin\u002Findex",[2076,6641,6642,6643,6644],"MCP","Gin","AI Agent","后端","2026-06-01","5fZl1Vigs2lHb5fHPz-0YZFyFD38ws1heOouZUG1g6M",{"id":6648,"title":6649,"author":6,"body":6650,"description":7417,"difficulty":6,"draft":1260,"extension":1261,"image":7418,"meta":7419,"navigation":666,"path":7420,"pinned":1260,"published":7421,"seo":7422,"series":6,"seriesOrder":6,"stem":7423,"tags":7424,"updated":6,"__hash__":7428},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":8,"value":6651,"toc":7403},[6652,6656,6659,6666,6792,6795,6798,6854,6857,6872,6875,6878,6885,7117,7124,7128,7131,7151,7154,7169,7172,7192,7195,7198,7205,7271,7274,7277,7281,7284,7336,7340,7361,7365,7368,7382,7385,7389,7392,7394,7397,7400],[11,6653,6655],{"id":6654},"dws-是什么","dws 是什么",[15,6657,6658],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[15,6660,6661,6662,6665],{},"它的定位很明确：",[19,6663,6664],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[75,6667,6669],{"className":1291,"code":6668,"language":1293,"meta":81,"style":81},"# 查看今天有什么会\ndws calendar event list --start 2026-06-01 --end 2026-06-01\n\n# 给群里发条消息\ndws chat send --group-id xxx --content \"今天发布推迟到下午3点\"\n\n# 创建一个待办\ndws todo task create --subject \"Review PR #123\" --due-date 2026-06-02\n\n# 读取钉钉文档\ndws doc read --url \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[45,6670,6671,6676,6702,6706,6711,6733,6737,6742,6767,6771,6776],{"__ignoreMap":81},[213,6672,6673],{"class":215,"line":216},[213,6674,6675],{"class":219},"# 查看今天有什么会\n",[213,6677,6678,6681,6684,6687,6690,6693,6696,6699],{"class":215,"line":223},[213,6679,6680],{"class":237},"dws",[213,6682,6683],{"class":377}," calendar",[213,6685,6686],{"class":377}," event",[213,6688,6689],{"class":377}," list",[213,6691,6692],{"class":339}," --start",[213,6694,6695],{"class":377}," 2026-06-01",[213,6697,6698],{"class":339}," --end",[213,6700,6701],{"class":377}," 2026-06-01\n",[213,6703,6704],{"class":215,"line":244},[213,6705,667],{"emptyLinePlaceholder":666},[213,6707,6708],{"class":215,"line":360},[213,6709,6710],{"class":219},"# 给群里发条消息\n",[213,6712,6713,6715,6718,6721,6724,6727,6730],{"class":215,"line":395},[213,6714,6680],{"class":237},[213,6716,6717],{"class":377}," chat",[213,6719,6720],{"class":377}," send",[213,6722,6723],{"class":339}," --group-id",[213,6725,6726],{"class":377}," xxx",[213,6728,6729],{"class":339}," --content",[213,6731,6732],{"class":377}," \"今天发布推迟到下午3点\"\n",[213,6734,6735],{"class":215,"line":412},[213,6736,667],{"emptyLinePlaceholder":666},[213,6738,6739],{"class":215,"line":430},[213,6740,6741],{"class":219},"# 创建一个待办\n",[213,6743,6744,6746,6749,6752,6755,6758,6761,6764],{"class":215,"line":442},[213,6745,6680],{"class":237},[213,6747,6748],{"class":377}," todo",[213,6750,6751],{"class":377}," task",[213,6753,6754],{"class":377}," create",[213,6756,6757],{"class":339}," --subject",[213,6759,6760],{"class":377}," \"Review PR #123\"",[213,6762,6763],{"class":339}," --due-date",[213,6765,6766],{"class":377}," 2026-06-02\n",[213,6768,6769],{"class":215,"line":448},[213,6770,667],{"emptyLinePlaceholder":666},[213,6772,6773],{"class":215,"line":461},[213,6774,6775],{"class":219},"# 读取钉钉文档\n",[213,6777,6778,6780,6783,6786,6789],{"class":215,"line":751},[213,6779,6680],{"class":237},[213,6781,6782],{"class":377}," doc",[213,6784,6785],{"class":377}," read",[213,6787,6788],{"class":339}," --url",[213,6790,6791],{"class":377}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[11,6793,6794],{"id":6794},"安装",[15,6796,6797],{},"三种方式，选你喜欢的：",[75,6799,6801],{"className":1291,"code":6800,"language":1293,"meta":81,"style":81},"# npm（推荐，跨平台自动下载二进制）\nnpm install -g dingtalk-workspace-cli\n\n# Homebrew（macOS\u002FLinux）\nbrew install dws\n\n# 直接下载二进制\n# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[45,6802,6803,6808,6821,6825,6830,6840,6844,6849],{"__ignoreMap":81},[213,6804,6805],{"class":215,"line":216},[213,6806,6807],{"class":219},"# npm（推荐，跨平台自动下载二进制）\n",[213,6809,6810,6813,6815,6818],{"class":215,"line":223},[213,6811,6812],{"class":237},"npm",[213,6814,1363],{"class":377},[213,6816,6817],{"class":339}," -g",[213,6819,6820],{"class":377}," dingtalk-workspace-cli\n",[213,6822,6823],{"class":215,"line":244},[213,6824,667],{"emptyLinePlaceholder":666},[213,6826,6827],{"class":215,"line":360},[213,6828,6829],{"class":219},"# Homebrew（macOS\u002FLinux）\n",[213,6831,6832,6835,6837],{"class":215,"line":395},[213,6833,6834],{"class":237},"brew",[213,6836,1363],{"class":377},[213,6838,6839],{"class":377}," dws\n",[213,6841,6842],{"class":215,"line":412},[213,6843,667],{"emptyLinePlaceholder":666},[213,6845,6846],{"class":215,"line":430},[213,6847,6848],{"class":219},"# 直接下载二进制\n",[213,6850,6851],{"class":215,"line":442},[213,6852,6853],{"class":219},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[15,6855,6856],{},"安装后认证：",[75,6858,6860],{"className":1291,"code":6859,"language":1293,"meta":81,"style":81},"dws auth login\n",[45,6861,6862],{"__ignoreMap":81},[213,6863,6864,6866,6869],{"class":215,"line":216},[213,6865,6680],{"class":237},[213,6867,6868],{"class":377}," auth",[213,6870,6871],{"class":377}," login\n",[15,6873,6874],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[11,6876,6877],{"id":6877},"覆盖的钉钉产品",[15,6879,6880,6881,6884],{},"这是最夸张的部分 — ",[19,6882,6883],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[249,6886,6887,6899],{},[252,6888,6889],{},[255,6890,6891,6894,6897],{},[258,6892,6893],{},"产品",[258,6895,6896],{},"命令数",[258,6898,6547],{},[265,6900,6901,6915,6929,6943,6957,6971,6985,6999,7012,7026,7039,7053,7067,7080,7093,7106],{},[255,6902,6903,6909,6912],{},[270,6904,6905,6906],{},"聊天 ",[45,6907,6908],{},"chat",[270,6910,6911],{},"57",[270,6913,6914],{},"消息收发、群管理、机器人、已读状态、Webhook",[255,6916,6917,6923,6926],{},[270,6918,6919,6920],{},"AI 表格 ",[45,6921,6922],{},"aitable",[270,6924,6925],{},"42",[270,6927,6928],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[255,6930,6931,6937,6940],{},[270,6932,6933,6934],{},"电子表格 ",[45,6935,6936],{},"sheet",[270,6938,6939],{},"34",[270,6941,6942],{},"工作表读写、行列操作、合并、筛选视图、导出",[255,6944,6945,6951,6954],{},[270,6946,6947,6948],{},"文档 ",[45,6949,6950],{},"doc",[270,6952,6953],{},"21",[270,6955,6956],{},"搜索、读写、块级编辑、评论、复制移动",[255,6958,6959,6965,6968],{},[270,6960,6961,6962],{},"会议纪要 ",[45,6963,6964],{},"minutes",[270,6966,6967],{},"19",[270,6969,6970],{},"列表、摘要、转写、思维导图、发言人、热词",[255,6972,6973,6979,6982],{},[270,6974,6975,6976],{},"日历 ",[45,6977,6978],{},"calendar",[270,6980,6981],{},"14",[270,6983,6984],{},"事件 CRUD、会议室、忙闲查询、时间建议",[255,6986,6987,6993,6996],{},[270,6988,6989,6990],{},"审批 ",[45,6991,6992],{},"oa",[270,6994,6995],{},"9",[270,6997,6998],{},"待办、发起、同意\u002F拒绝\u002F撤销",[255,7000,7001,7007,7009],{},[270,7002,7003,7004],{},"云盘 ",[45,7005,7006],{},"drive",[270,7008,6995],{},[270,7010,7011],{},"列表、下载、上传、创建目录",[255,7013,7014,7020,7023],{},[270,7015,7016,7017],{},"知识库 ",[45,7018,7019],{},"wiki",[270,7021,7022],{},"7",[270,7024,7025],{},"空间管理、成员管理",[255,7027,7028,7034,7036],{},[270,7029,7030,7031],{},"汇报 ",[45,7032,7033],{},"report",[270,7035,7022],{},[270,7037,7038],{},"日志\u002F日报\u002F周报，按模板创建、统计",[255,7040,7041,7047,7050],{},[270,7042,7043,7044],{},"待办 ",[45,7045,7046],{},"todo",[270,7048,7049],{},"6",[270,7051,7052],{},"创建、完成、删除，支持优先级和循环",[255,7054,7055,7061,7064],{},[270,7056,7057,7058],{},"邮件 ",[45,7059,7060],{},"mail",[270,7062,7063],{},"4",[270,7065,7066],{},"邮箱查询、邮件搜索、发送",[255,7068,7069,7075,7077],{},[270,7070,7071,7072],{},"考勤 ",[45,7073,7074],{},"attendance",[270,7076,7063],{},[270,7078,7079],{},"打卡记录、排班、汇总",[255,7081,7082,7088,7090],{},[270,7083,7084,7085],{},"通讯录 ",[45,7086,7087],{},"contact",[270,7089,340],{},[270,7091,7092],{},"用户搜索、部门查询",[255,7094,7095,7101,7103],{},[270,7096,7097,7098],{},"DING 消息 ",[45,7099,7100],{},"ding",[270,7102,3456],{},[270,7104,7105],{},"发送\u002F撤回（应用内、短信、电话）",[255,7107,7108,7111,7114],{},[270,7109,7110],{},"其他",[270,7112,7113],{},"若干",[270,7115,7116],{},"AI 搜索、AI 应用、直播、开放平台文档",[15,7118,7119,7120,7123],{},"还有一个万能命令 ",[45,7121,7122],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[11,7125,7127],{"id":7126},"agent-skill-系统","Agent Skill 系统",[15,7129,7130],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[177,7132,7133,7139,7145],{},[29,7134,7135,7138],{},[19,7136,7137],{},"Claude Code"," — 通过 SKILL.md 自动加载",[29,7140,7141,7144],{},[19,7142,7143],{},"Cursor"," — 支持 MCP 协议接入",[29,7146,7147,7150],{},[19,7148,7149],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[15,7152,7153],{},"安装 skill：",[75,7155,7157],{"className":1291,"code":7156,"language":1293,"meta":81,"style":81},"dws skill setup\n",[45,7158,7159],{"__ignoreMap":81},[213,7160,7161,7163,7166],{"class":215,"line":216},[213,7162,6680],{"class":237},[213,7164,7165],{"class":377}," skill",[213,7167,7168],{"class":377}," setup\n",[15,7170,7171],{},"SKILL.md 里定义了：",[177,7173,7174,7183,7186,7189],{},[29,7175,7176,7177,7179,7180,7182],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[45,7178,6922],{},"，说\"打卡\"→ ",[45,7181,7074],{},"）",[29,7184,7185],{},"危险操作确认流程（删除、撤回等必须先确认）",[29,7187,7188],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[29,7190,7191],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[15,7193,7194],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[11,7196,7197],{"id":7197},"自动升级",[15,7199,7200,7201,7204],{},"内置 ",[45,7202,7203],{},"dws upgrade","，不需要重新下载安装包：",[75,7206,7208],{"className":1291,"code":7207,"language":1293,"meta":81,"style":81},"dws upgrade              # 交互式升级到最新版\ndws upgrade --check      # 仅检查是否有新版本\ndws upgrade --list       # 列出最近版本\ndws upgrade --version v1.0.5  # 升级到指定版本\ndws upgrade --rollback   # 回滚到上一版本\n",[45,7209,7210,7220,7232,7244,7259],{"__ignoreMap":81},[213,7211,7212,7214,7217],{"class":215,"line":216},[213,7213,6680],{"class":237},[213,7215,7216],{"class":377}," upgrade",[213,7218,7219],{"class":219},"              # 交互式升级到最新版\n",[213,7221,7222,7224,7226,7229],{"class":215,"line":223},[213,7223,6680],{"class":237},[213,7225,7216],{"class":377},[213,7227,7228],{"class":339}," --check",[213,7230,7231],{"class":219},"      # 仅检查是否有新版本\n",[213,7233,7234,7236,7238,7241],{"class":215,"line":244},[213,7235,6680],{"class":237},[213,7237,7216],{"class":377},[213,7239,7240],{"class":339}," --list",[213,7242,7243],{"class":219},"       # 列出最近版本\n",[213,7245,7246,7248,7250,7253,7256],{"class":215,"line":360},[213,7247,6680],{"class":237},[213,7249,7216],{"class":377},[213,7251,7252],{"class":339}," --version",[213,7254,7255],{"class":377}," v1.0.5",[213,7257,7258],{"class":219},"  # 升级到指定版本\n",[213,7260,7261,7263,7265,7268],{"class":215,"line":395},[213,7262,6680],{"class":237},[213,7264,7216],{"class":377},[213,7266,7267],{"class":339}," --rollback",[213,7269,7270],{"class":219},"   # 回滚到上一版本\n",[15,7272,7273],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[11,7275,7276],{"id":7276},"亮点",[113,7278,7280],{"id":7279},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[15,7282,7283],{},"这可能是目前对 AI 最友好的 CLI 工具：",[177,7285,7286,7307,7317,7326],{},[29,7287,7288,7291,7292,7295,7296,7299,7300,7295,7303,7306],{},[19,7289,7290],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[45,7293,7294],{},"--userId"," 应该是 ",[45,7297,7298],{},"--user-id","，",[45,7301,7302],{},"--limit100",[45,7304,7305],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[29,7308,7309,7312,7313,7316],{},[19,7310,7311],{},"Schema 发现"," — ",[45,7314,7315],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[29,7318,7319,7312,7322,7325],{},[19,7320,7321],{},"jq 过滤",[45,7323,7324],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[29,7327,7328,7331,7332,7335],{},[19,7329,7330],{},"JSON 输出"," — 所有命令支持 ",[45,7333,7334],{},"--format json","，方便程序解析",[113,7337,7339],{"id":7338},"_2-安全设计","2. 安全设计",[177,7341,7342,7345,7348,7351,7358],{},[29,7343,7344],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[29,7346,7347],{},"域名白名单，只能访问钉钉官方 API",[29,7349,7350],{},"强制 HTTPS",[29,7352,7353,7354,7357],{},"写操作必须显式 ",[45,7355,7356],{},"--yes"," 确认",[29,7359,7360],{},"完整审计链",[113,7362,7364],{"id":7363},"_3-发现驱动架构","3. 发现驱动架构",[15,7366,7367],{},"不是把所有命令硬编码在代码里，而是：",[26,7369,7370,7373,7376,7379],{},[29,7371,7372],{},"从钉钉 MCP 服务器注册中心拉取元数据",[29,7374,7375],{},"运行时发现服务端能力",[29,7377,7378],{},"转换成统一的中间表示（IR）",[29,7380,7381],{},"动态挂载到命令树",[15,7383,7384],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[113,7386,7388],{"id":7387},"_4-多平台分发","4. 多平台分发",[15,7390,7391],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[11,7393,2439],{"id":2439},[15,7395,7396],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[15,7398,7399],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[1231,7401,7402],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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":81,"searchDepth":223,"depth":244,"links":7404},[7405,7406,7407,7408,7409,7410,7416],{"id":6654,"depth":223,"text":6655},{"id":6794,"depth":223,"text":6794},{"id":6877,"depth":223,"text":6877},{"id":7126,"depth":223,"text":7127},{"id":7197,"depth":223,"text":7197},{"id":7276,"depth":223,"text":7276,"children":7411},[7412,7413,7414,7415],{"id":7279,"depth":244,"text":7280},{"id":7338,"depth":244,"text":7339},{"id":7363,"depth":244,"text":7364},{"id":7387,"depth":244,"text":7388},{"id":2439,"depth":223,"text":2439},"介绍阿里巴巴官方的 DingTalk Workspace CLI（dws），一个覆盖钉钉 19 个产品、213 个命令的 AI 友好命令行工具，支持自动升级和 Agent Skill 系统。","\u002Fposts\u002Fdws-dingtalk-cli\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdws-dingtalk-cli","2026-06-01T00:51:00",{"title":6649,"description":7417},"posts\u002Fdws-dingtalk-cli\u002Findex",[7425,7426,6643,2076,7427],"钉钉","CLI","效率工具","4ca65VUZ2Sz9yG8dNtftrBDDHE6QowA3ed9g4egQkKI",{"id":7430,"title":7431,"author":6,"body":7432,"description":8392,"difficulty":2066,"draft":1260,"extension":1261,"image":8393,"meta":8394,"navigation":666,"path":8395,"pinned":1260,"published":8396,"seo":8397,"series":6638,"seriesOrder":223,"stem":8398,"tags":8399,"updated":8403,"__hash__":8404},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",{"type":8,"value":7433,"toc":8375},[7434,7436,7443,7446,7449,7460,7463,7467,7797,7801,7909,7913,8047,8050,8054,8063,8080,8091,8110,8124,8130,8138,8237,8243,8247,8254,8263,8266,8302,8305,8310,8349,8351,8357,8369,8372],[11,7435,3144],{"id":3144},[15,7437,7438,7439,7442],{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[45,7440,7441],{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[15,7444,7445],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[11,7447,7448],{"id":7448},"核心思路",[15,7450,7451,7452,7455,7456,7459],{},"利用 Go 1.16 引入的 ",[45,7453,7454],{},"\u002F\u002Fgo:embed"," 指令，在编译时将前端 ",[45,7457,7458],{},"dist\u002F"," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[11,7461,7462],{"id":7462},"逐步实现",[113,7464,7466],{"id":7465},"_1-创建嵌入模块","1. 创建嵌入模块",[75,7468,7470],{"className":207,"code":7469,"language":209,"meta":81,"style":81},"\u002F\u002F service\u002Fweb\u002Fembed.go\npackage web\n\nimport (\n    \"embed\"\n    \"io\u002Ffs\"\n    \"mime\"\n    \"net\u002Fhttp\"\n    \"path\u002Ffilepath\"\n    \"strings\"\n\n    \"github.com\u002Fgin-gonic\u002Fgin\"\n)\n\n\u002F\u002Fgo:embed dist\nvar embedFS embed.FS\n\nfunc SPAHandler() gin.HandlerFunc {\n    sub, _ := fs.Sub(embedFS, \"dist\")\n\n    return func(c *gin.Context) {\n        p := strings.TrimPrefix(c.Request.URL.Path, \"\u002F\")\n        if p == \"\" {\n            p = \"index.html\"\n        }\n\n        data, err := fs.ReadFile(sub, p)\n        if err != nil {\n            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n            data, _ = fs.ReadFile(sub, \"index.html\")\n        }\n\n        c.Data(http.StatusOK, mime.TypeByExtension(filepath.Ext(p)), data)\n    }\n}\n",[45,7471,7472,7477,7485,7489,7495,7504,7513,7522,7531,7540,7549,7553,7562,7566,7570,7575,7589,7593,7610,7631,7635,7655,7675,7688,7698,7702,7706,7720,7732,7737,7756,7760,7764,7787,7792],{"__ignoreMap":81},[213,7473,7474],{"class":215,"line":216},[213,7475,7476],{"class":219},"\u002F\u002F service\u002Fweb\u002Fembed.go\n",[213,7478,7479,7482],{"class":215,"line":223},[213,7480,7481],{"class":230},"package",[213,7483,7484],{"class":237}," web\n",[213,7486,7487],{"class":215,"line":244},[213,7488,667],{"emptyLinePlaceholder":666},[213,7490,7491,7493],{"class":215,"line":360},[213,7492,634],{"class":230},[213,7494,637],{"class":226},[213,7496,7497,7499,7502],{"class":215,"line":395},[213,7498,642],{"class":377},[213,7500,7501],{"class":237},"embed",[213,7503,648],{"class":377},[213,7505,7506,7508,7511],{"class":215,"line":412},[213,7507,642],{"class":377},[213,7509,7510],{"class":237},"io\u002Ffs",[213,7512,648],{"class":377},[213,7514,7515,7517,7520],{"class":215,"line":430},[213,7516,642],{"class":377},[213,7518,7519],{"class":237},"mime",[213,7521,648],{"class":377},[213,7523,7524,7526,7529],{"class":215,"line":442},[213,7525,642],{"class":377},[213,7527,7528],{"class":237},"net\u002Fhttp",[213,7530,648],{"class":377},[213,7532,7533,7535,7538],{"class":215,"line":448},[213,7534,642],{"class":377},[213,7536,7537],{"class":237},"path\u002Ffilepath",[213,7539,648],{"class":377},[213,7541,7542,7544,7547],{"class":215,"line":461},[213,7543,642],{"class":377},[213,7545,7546],{"class":237},"strings",[213,7548,648],{"class":377},[213,7550,7551],{"class":215,"line":751},[213,7552,667],{"emptyLinePlaceholder":666},[213,7554,7555,7557,7560],{"class":215,"line":765},[213,7556,642],{"class":377},[213,7558,7559],{"class":237},"github.com\u002Fgin-gonic\u002Fgin",[213,7561,648],{"class":377},[213,7563,7564],{"class":215,"line":774},[213,7565,392],{"class":226},[213,7567,7568],{"class":215,"line":780},[213,7569,667],{"emptyLinePlaceholder":666},[213,7571,7572],{"class":215,"line":797},[213,7573,7574],{"class":219},"\u002F\u002Fgo:embed dist\n",[213,7576,7577,7579,7582,7584,7586],{"class":215,"line":810},[213,7578,677],{"class":230},[213,7580,7581],{"class":226}," embedFS ",[213,7583,7501],{"class":237},[213,7585,686],{"class":226},[213,7587,7588],{"class":237},"FS\n",[213,7590,7591],{"class":215,"line":817},[213,7592,667],{"emptyLinePlaceholder":666},[213,7594,7595,7597,7600,7602,7604,7606,7608],{"class":215,"line":822},[213,7596,712],{"class":230},[213,7598,7599],{"class":237}," SPAHandler",[213,7601,718],{"class":226},[213,7603,5505],{"class":237},[213,7605,686],{"class":226},[213,7607,5723],{"class":237},[213,7609,427],{"class":226},[213,7611,7612,7615,7617,7620,7623,7626,7629],{"class":215,"line":834},[213,7613,7614],{"class":226},"    sub, _ ",[213,7616,231],{"class":230},[213,7618,7619],{"class":226}," fs.",[213,7621,7622],{"class":237},"Sub",[213,7624,7625],{"class":226},"(embedFS, ",[213,7627,7628],{"class":377},"\"dist\"",[213,7630,392],{"class":226},[213,7632,7633],{"class":215,"line":842},[213,7634,667],{"emptyLinePlaceholder":666},[213,7636,7637,7639,7641,7643,7645,7647,7649,7651,7653],{"class":215,"line":1714},[213,7638,433],{"class":230},[213,7640,5732],{"class":230},[213,7642,739],{"class":226},[213,7644,5499],{"class":865},[213,7646,5502],{"class":230},[213,7648,5505],{"class":237},[213,7650,686],{"class":226},[213,7652,4897],{"class":237},[213,7654,4515],{"class":226},[213,7656,7657,7660,7662,7664,7667,7670,7673],{"class":215,"line":1719},[213,7658,7659],{"class":226},"        p ",[213,7661,231],{"class":230},[213,7663,5839],{"class":226},[213,7665,7666],{"class":237},"TrimPrefix",[213,7668,7669],{"class":226},"(c.Request.URL.Path, ",[213,7671,7672],{"class":377},"\"\u002F\"",[213,7674,392],{"class":226},[213,7676,7677,7679,7682,7684,7686],{"class":215,"line":2350},[213,7678,5771],{"class":230},[213,7680,7681],{"class":226}," p ",[213,7683,5777],{"class":230},[213,7685,5780],{"class":377},[213,7687,427],{"class":226},[213,7689,7690,7693,7695],{"class":215,"line":2364},[213,7691,7692],{"class":226},"            p ",[213,7694,1386],{"class":230},[213,7696,7697],{"class":377}," \"index.html\"\n",[213,7699,7700],{"class":215,"line":2371},[213,7701,5825],{"class":226},[213,7703,7704],{"class":215,"line":2376},[213,7705,667],{"emptyLinePlaceholder":666},[213,7707,7708,7711,7713,7715,7717],{"class":215,"line":2382},[213,7709,7710],{"class":226},"        data, err ",[213,7712,231],{"class":230},[213,7714,7619],{"class":226},[213,7716,736],{"class":237},[213,7718,7719],{"class":226},"(sub, p)\n",[213,7721,7722,7724,7726,7728,7730],{"class":215,"line":2390},[213,7723,5771],{"class":230},[213,7725,418],{"class":226},[213,7727,421],{"class":230},[213,7729,424],{"class":339},[213,7731,427],{"class":226},[213,7733,7734],{"class":215,"line":2398},[213,7735,7736],{"class":219},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[213,7738,7739,7742,7744,7746,7748,7751,7754],{"class":215,"line":2403},[213,7740,7741],{"class":226},"            data, _ ",[213,7743,1386],{"class":230},[213,7745,7619],{"class":226},[213,7747,736],{"class":237},[213,7749,7750],{"class":226},"(sub, ",[213,7752,7753],{"class":377},"\"index.html\"",[213,7755,392],{"class":226},[213,7757,7758],{"class":215,"line":2409},[213,7759,5825],{"class":226},[213,7761,7762],{"class":215,"line":2418},[213,7763,667],{"emptyLinePlaceholder":666},[213,7765,7767,7769,7772,7775,7778,7781,7784],{"class":215,"line":7766},33,[213,7768,5516],{"class":226},[213,7770,7771],{"class":237},"Data",[213,7773,7774],{"class":226},"(http.StatusOK, mime.",[213,7776,7777],{"class":237},"TypeByExtension",[213,7779,7780],{"class":226},"(filepath.",[213,7782,7783],{"class":237},"Ext",[213,7785,7786],{"class":226},"(p)), data)\n",[213,7788,7790],{"class":215,"line":7789},34,[213,7791,777],{"class":226},[213,7793,7795],{"class":215,"line":7794},35,[213,7796,445],{"class":226},[113,7798,7800],{"id":7799},"_2-改造路由","2. 改造路由",[75,7802,7804],{"className":207,"code":7803,"language":209,"meta":81,"style":81},"\u002F\u002F router.go\nimport \"myproject\u002Fweb\"\n\n\u002F\u002F 替换原来的磁盘静态文件服务\nwebHandler := web.SPAHandler()\nrouter.GET(\"\u002F\", webHandler)\nrouter.GET(\"\u002Fassets\u002F*filepath\", webHandler)\nrouter.GET(\"\u002Ffavicon.ico\", webHandler)\nrouter.GET(\"\u002Ffavicon.svg\", webHandler)\nrouter.NoRoute(webHandler)\n",[45,7805,7806,7811,7822,7826,7831,7846,7860,7873,7886,7899],{"__ignoreMap":81},[213,7807,7808],{"class":215,"line":216},[213,7809,7810],{"class":219},"\u002F\u002F router.go\n",[213,7812,7813,7815,7817,7820],{"class":215,"line":223},[213,7814,634],{"class":230},[213,7816,1320],{"class":377},[213,7818,7819],{"class":237},"myproject\u002Fweb",[213,7821,648],{"class":377},[213,7823,7824],{"class":215,"line":244},[213,7825,667],{"emptyLinePlaceholder":666},[213,7827,7828],{"class":215,"line":360},[213,7829,7830],{"class":219},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[213,7832,7833,7836,7838,7841,7844],{"class":215,"line":395},[213,7834,7835],{"class":226},"webHandler ",[213,7837,231],{"class":230},[213,7839,7840],{"class":226}," web.",[213,7842,7843],{"class":237},"SPAHandler",[213,7845,357],{"class":226},[213,7847,7848,7851,7853,7855,7857],{"class":215,"line":412},[213,7849,7850],{"class":226},"router.",[213,7852,5485],{"class":237},[213,7854,739],{"class":226},[213,7856,7672],{"class":377},[213,7858,7859],{"class":226},", webHandler)\n",[213,7861,7862,7864,7866,7868,7871],{"class":215,"line":430},[213,7863,7850],{"class":226},[213,7865,5485],{"class":237},[213,7867,739],{"class":226},[213,7869,7870],{"class":377},"\"\u002Fassets\u002F*filepath\"",[213,7872,7859],{"class":226},[213,7874,7875,7877,7879,7881,7884],{"class":215,"line":442},[213,7876,7850],{"class":226},[213,7878,5485],{"class":237},[213,7880,739],{"class":226},[213,7882,7883],{"class":377},"\"\u002Ffavicon.ico\"",[213,7885,7859],{"class":226},[213,7887,7888,7890,7892,7894,7897],{"class":215,"line":448},[213,7889,7850],{"class":226},[213,7891,5485],{"class":237},[213,7893,739],{"class":226},[213,7895,7896],{"class":377},"\"\u002Ffavicon.svg\"",[213,7898,7859],{"class":226},[213,7900,7901,7903,7906],{"class":215,"line":461},[213,7902,7850],{"class":226},[213,7904,7905],{"class":237},"NoRoute",[213,7907,7908],{"class":226},"(webHandler)\n",[113,7910,7912],{"id":7911},"_3-一键构建脚本","3. 一键构建脚本",[75,7914,7916],{"className":1291,"code":7915,"language":1293,"meta":81,"style":81},"#!\u002Fbin\u002Fbash\n# build-standalone.sh\n\nset -e\n\n# 1. 构建前端\npnpm install --no-frozen-lockfile\nnpx vite build\n\n# 2. 复制到嵌入目录\nrm -rf service\u002Fweb\u002Fdist\ncp -r dist service\u002Fweb\u002Fdist\n\n# 3. 编译 Go 二进制\ncd service\ngo build -o myapp.exe \\\n    -ldflags=\"-X myproject\u002Fglobal.RUNCODE=release\" \\\n    main.go\n",[45,7917,7918,7923,7928,7932,7940,7944,7949,7959,7969,7973,7978,7989,8002,8006,8011,8019,8032,8042],{"__ignoreMap":81},[213,7919,7920],{"class":215,"line":216},[213,7921,7922],{"class":219},"#!\u002Fbin\u002Fbash\n",[213,7924,7925],{"class":215,"line":223},[213,7926,7927],{"class":219},"# build-standalone.sh\n",[213,7929,7930],{"class":215,"line":244},[213,7931,667],{"emptyLinePlaceholder":666},[213,7933,7934,7937],{"class":215,"line":360},[213,7935,7936],{"class":339},"set",[213,7938,7939],{"class":339}," -e\n",[213,7941,7942],{"class":215,"line":395},[213,7943,667],{"emptyLinePlaceholder":666},[213,7945,7946],{"class":215,"line":412},[213,7947,7948],{"class":219},"# 1. 构建前端\n",[213,7950,7951,7954,7956],{"class":215,"line":430},[213,7952,7953],{"class":237},"pnpm",[213,7955,1363],{"class":377},[213,7957,7958],{"class":339}," --no-frozen-lockfile\n",[213,7960,7961,7963,7966],{"class":215,"line":442},[213,7962,3042],{"class":237},[213,7964,7965],{"class":377}," vite",[213,7967,7968],{"class":377}," build\n",[213,7970,7971],{"class":215,"line":448},[213,7972,667],{"emptyLinePlaceholder":666},[213,7974,7975],{"class":215,"line":461},[213,7976,7977],{"class":219},"# 2. 复制到嵌入目录\n",[213,7979,7980,7983,7986],{"class":215,"line":751},[213,7981,7982],{"class":237},"rm",[213,7984,7985],{"class":339}," -rf",[213,7987,7988],{"class":377}," service\u002Fweb\u002Fdist\n",[213,7990,7991,7994,7997,8000],{"class":215,"line":765},[213,7992,7993],{"class":237},"cp",[213,7995,7996],{"class":339}," -r",[213,7998,7999],{"class":377}," dist",[213,8001,7988],{"class":377},[213,8003,8004],{"class":215,"line":774},[213,8005,667],{"emptyLinePlaceholder":666},[213,8007,8008],{"class":215,"line":780},[213,8009,8010],{"class":219},"# 3. 编译 Go 二进制\n",[213,8012,8013,8016],{"class":215,"line":797},[213,8014,8015],{"class":339},"cd",[213,8017,8018],{"class":377}," service\n",[213,8020,8021,8023,8025,8027,8030],{"class":215,"line":810},[213,8022,209],{"class":237},[213,8024,1431],{"class":377},[213,8026,1440],{"class":339},[213,8028,8029],{"class":377}," myapp.exe",[213,8031,5603],{"class":339},[213,8033,8034,8037,8040],{"class":215,"line":817},[213,8035,8036],{"class":339},"    -ldflags=",[213,8038,8039],{"class":377},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[213,8041,5603],{"class":339},[213,8043,8044],{"class":215,"line":822},[213,8045,8046],{"class":377},"    main.go\n",[11,8048,8049],{"id":8049},"踩坑记录",[113,8051,8053],{"id":8052},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[15,8055,8056,8057,8059,8060,8062],{},"首次实现时用了 Gin 的 ",[45,8058,7905],{}," 兜底，但部分静态资源返回 404。排查发现 ",[45,8061,7905],{}," 在某些路由未显式注册时不会按预期触发。",[15,8064,8065,8068,8069,8072,8073,8076,8077,8079],{},[19,8066,8067],{},"解决","：显式注册 ",[45,8070,8071],{},"\u002Fassets\u002F*filepath","、",[45,8074,8075],{},"\u002Ffavicon.ico"," 等路由，",[45,8078,7905],{}," 仅作为兜底。",[113,8081,8083,8084,8086,8087,8090],{"id":8082},"坑二goembed-排除-_-开头文件","坑二：",[45,8085,7454],{}," 排除 ",[45,8088,8089],{},"_"," 开头文件",[15,8092,8093,8094,8097,8098,8100,8101,59],{},"这是最坑的。Vite 构建产出了一个 ",[45,8095,8096],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[45,8099,7454],{}," 当模式为目录时，",[19,8102,8103,8104,8106,8107,8109],{},"硬编码排除了 ",[45,8105,8089],{}," 和 ",[45,8108,686],{}," 开头的文件",[75,8111,8113],{"className":207,"code":8112,"language":209,"meta":81,"style":81},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[45,8114,8115,8120],{"__ignoreMap":81},[213,8116,8117],{"class":215,"line":216},[213,8118,8119],{"class":219},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[213,8121,8122],{"class":215,"line":223},[213,8123,7574],{"class":219},[15,8125,8126,8127,59],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[45,8128,8129],{},"Unexpected token '\u003C'",[15,8131,8132,8134,8135,8137],{},[19,8133,8067],{},"：改 Vite 配置，去除了 ",[45,8136,8089],{}," 前缀：",[75,8139,8143],{"className":8140,"code":8141,"language":8142,"meta":81,"style":81},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F vite.config.ts\nbuild: {\n  rollupOptions: {\n    output: {\n      chunkFileNames(chunkInfo) {\n        return `assets\u002F${chunkInfo.name.replace(\u002F^_\u002F, '')}-[hash].js`\n      },\n    },\n  },\n},\n","ts",[45,8144,8145,8150,8157,8164,8171,8179,8217,8222,8227,8232],{"__ignoreMap":81},[213,8146,8147],{"class":215,"line":216},[213,8148,8149],{"class":219},"\u002F\u002F vite.config.ts\n",[213,8151,8152,8155],{"class":215,"line":223},[213,8153,8154],{"class":237},"build",[213,8156,3195],{"class":226},[213,8158,8159,8162],{"class":215,"line":244},[213,8160,8161],{"class":237},"  rollupOptions",[213,8163,3195],{"class":226},[213,8165,8166,8169],{"class":215,"line":360},[213,8167,8168],{"class":237},"    output",[213,8170,3195],{"class":226},[213,8172,8173,8176],{"class":215,"line":395},[213,8174,8175],{"class":237},"      chunkFileNames",[213,8177,8178],{"class":226},"(chunkInfo) {\n",[213,8180,8181,8183,8186,8189,8191,8194,8196,8198,8200,8202,8204,8206,8208,8210,8212,8214],{"class":215,"line":412},[213,8182,768],{"class":230},[213,8184,8185],{"class":377}," `assets\u002F${",[213,8187,8188],{"class":226},"chunkInfo",[213,8190,686],{"class":377},[213,8192,8193],{"class":226},"name",[213,8195,686],{"class":377},[213,8197,3767],{"class":237},[213,8199,739],{"class":377},[213,8201,3390],{"class":377},[213,8203,3377],{"class":230},[213,8205,8089],{"class":3380},[213,8207,3390],{"class":377},[213,8209,381],{"class":377},[213,8211,3784],{"class":377},[213,8213,4085],{"class":377},[213,8215,8216],{"class":377},"}-[hash].js`\n",[213,8218,8219],{"class":215,"line":430},[213,8220,8221],{"class":226},"      },\n",[213,8223,8224],{"class":215,"line":442},[213,8225,8226],{"class":226},"    },\n",[213,8228,8229],{"class":215,"line":448},[213,8230,8231],{"class":226},"  },\n",[213,8233,8234],{"class":215,"line":461},[213,8235,8236],{"class":226},"},\n",[15,8238,8239,8240,8242],{},"这样就避免产生 ",[45,8241,8089],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[113,8244,8246],{"id":8245},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[15,8248,8249,8250,8253],{},"多次 ",[45,8251,8252],{},"go build"," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[15,8255,8256,8258,8259,8262],{},[19,8257,8067],{},"：使用 ",[45,8260,8261],{},"go build -a"," 强制重新编译所有包。",[11,8264,8265],{"id":8265},"最终效果",[75,8267,8269],{"className":1291,"code":8268,"language":1293,"meta":81,"style":81},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[45,8270,8271,8281,8291],{"__ignoreMap":81},[213,8272,8273,8275,8278],{"class":215,"line":216},[213,8274,3387],{"class":237},[213,8276,8277],{"class":377}," bash",[213,8279,8280],{"class":377}," build-standalone.sh\n",[213,8282,8283,8285,8288],{"class":215,"line":223},[213,8284,3365],{"class":377},[213,8286,8287],{"class":377}," 构建成功",[213,8289,8290],{"class":377}," ===\n",[213,8292,8293,8296,8299],{"class":215,"line":244},[213,8294,8295],{"class":237},"输出:",[213,8297,8298],{"class":377}," service\u002Fmyapp.exe",[213,8300,8301],{"class":377},"  41M\n",[15,8303,8304],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[15,8306,8307],{},[19,8308,8309],{},"部署对比：",[249,8311,8312,8325],{},[252,8313,8314],{},[255,8315,8316,8319,8322],{},[258,8317,8318],{},"方式",[258,8320,8321],{},"文件数",[258,8323,8324],{},"操作",[265,8326,8327,8338],{},[255,8328,8329,8332,8335],{},[270,8330,8331],{},"旧方案",[270,8333,8334],{},"1 exe + 1 web 目录",[270,8336,8337],{},"解压→保持目录结构",[255,8339,8340,8343,8346],{},[270,8341,8342],{},"新方案",[270,8344,8345],{},"1 个 exe",[270,8347,8348],{},"直接扔到服务器运行",[11,8350,2439],{"id":2439},[15,8352,8353,8354,8356],{},"Go 的 ",[45,8355,7454],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[26,8358,8359,8366],{},[29,8360,8361,8362,8106,8364,8109],{},"嵌入目录时自动排除 ",[45,8363,8089],{},[45,8365,686],{},[29,8367,8368],{},"构建缓存可能导致 embed 不是最新的",[15,8370,8371],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[1231,8373,8374],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sPKmS, html code.shiki .sPKmS{--shiki-light:#032F62;--shiki-dark:#DBEDFF}",{"title":81,"searchDepth":223,"depth":244,"links":8376},[8377,8378,8379,8384,8390,8391],{"id":3144,"depth":223,"text":3144},{"id":7448,"depth":223,"text":7448},{"id":7462,"depth":223,"text":7462,"children":8380},[8381,8382,8383],{"id":7465,"depth":244,"text":7466},{"id":7799,"depth":244,"text":7800},{"id":7911,"depth":244,"text":7912},{"id":8049,"depth":223,"text":8049,"children":8385},[8386,8387,8389],{"id":8052,"depth":244,"text":8053},{"id":8082,"depth":244,"text":8388},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":8245,"depth":244,"text":8246},{"id":8265,"depth":223,"text":8265},{"id":2439,"depth":223,"text":2439},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fgo-embed-frontend","2026-06-06T16:00:00",{"title":7431,"description":8392},"posts\u002Fgo-embed-frontend\u002Findex",[2076,8400,8401,8402],"Vue","编译","埋坑","2026-06-06","SIQNL24wVAFb18wIGWSB3lNVZUQ9B76jWgQ55TASTYk",{"id":8406,"title":8407,"author":6,"body":8408,"description":9804,"difficulty":1259,"draft":1260,"extension":1261,"image":9805,"meta":9806,"navigation":666,"path":9807,"pinned":1260,"published":9808,"seo":9809,"series":6,"seriesOrder":6,"stem":9810,"tags":9811,"updated":9808,"__hash__":9813},"posts\u002Fposts\u002Fjira-cli-intro\u002Findex.md","jira_cli — 把企业 Jira 装进命令行，AI Agent 直接调用",{"type":8,"value":8409,"toc":9783},[8410,8414,8429,8496,8499,8502,8579,8581,8614,8621,8624,8628,8740,8744,8875,8879,8947,8950,9013,9016,9018,9022,9025,9045,9048,9063,9066,9070,9076,9214,9225,9256,9260,9263,9360,9364,9367,9485,9489,9492,9556,9559,9563,9566,9634,9637,9643,9646,9680,9687,9690,9750,9752,9755,9772,9780],[11,8411,8413],{"id":8412},"jira_cli-是什么","jira_cli 是什么",[15,8415,8416,8421,8422,8106,8425,8428],{},[3097,8417,8420],{"href":8418,"rel":8419},"https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli",[3101],"jira_cli"," 是一个面向企业 Jira（Server \u002F Data Center）的本地命令行工具，Go 语言编写，MIT 协议开源。它同时提供 ",[19,8423,8424],{},"Cobra CLI",[19,8426,8427],{},"MCP stdio server"," 两种入口，既能当日常命令行用，又能直接接入 AI 编程工具。",[75,8430,8432],{"className":1291,"code":8431,"language":1293,"meta":81,"style":81},"# CLI 日常使用\njira_cli issue search \"project = PROJ ORDER BY updated DESC\"\njira_cli issue get PROJ-123\njira_cli issue comment PROJ-123 \"处理完成，请测试验证\"\n\n# MCP 模式 — 给 AI 装上操作 Jira 的手\njira_cli mcp --user=work\n",[45,8433,8434,8439,8452,8463,8478,8482,8487],{"__ignoreMap":81},[213,8435,8436],{"class":215,"line":216},[213,8437,8438],{"class":219},"# CLI 日常使用\n",[213,8440,8441,8443,8446,8449],{"class":215,"line":223},[213,8442,8420],{"class":237},[213,8444,8445],{"class":377}," issue",[213,8447,8448],{"class":377}," search",[213,8450,8451],{"class":377}," \"project = PROJ ORDER BY updated DESC\"\n",[213,8453,8454,8456,8458,8460],{"class":215,"line":244},[213,8455,8420],{"class":237},[213,8457,8445],{"class":377},[213,8459,4668],{"class":377},[213,8461,8462],{"class":377}," PROJ-123\n",[213,8464,8465,8467,8469,8472,8475],{"class":215,"line":360},[213,8466,8420],{"class":237},[213,8468,8445],{"class":377},[213,8470,8471],{"class":377}," comment",[213,8473,8474],{"class":377}," PROJ-123",[213,8476,8477],{"class":377}," \"处理完成，请测试验证\"\n",[213,8479,8480],{"class":215,"line":395},[213,8481,667],{"emptyLinePlaceholder":666},[213,8483,8484],{"class":215,"line":412},[213,8485,8486],{"class":219},"# MCP 模式 — 给 AI 装上操作 Jira 的手\n",[213,8488,8489,8491,8493],{"class":215,"line":430},[213,8490,8420],{"class":237},[213,8492,4905],{"class":377},[213,8494,8495],{"class":339}," --user=work\n",[11,8497,8498],{"id":8498},"为什么需要它",[15,8500,8501],{},"大多数 Jira 命令行工具只解决\"开发者手动操作 Jira\"这一个场景。但 2026 年 AI 编程工具已经普及，你的 AI Agent（Claude Code、Codex、Cursor 等）也需要读写 Jira。痛点就来了：",[249,8503,8504,8514],{},[252,8505,8506],{},[255,8507,8508,8511],{},[258,8509,8510],{},"痛点",[258,8512,8513],{},"jira_cli 怎么解",[265,8515,8516,8524,8532,8544,8556,8567],{},[255,8517,8518,8521],{},[270,8519,8520],{},"企业 Jira 在内网，没有公网 API",[270,8522,8523],{},"本地 stdio MCP server，不经过外网",[255,8525,8526,8529],{},[270,8527,8528],{},"AI 可能误删、误写 Jira 数据",[270,8530,8531],{},"账号级只读策略，AI 只能查不能改",[255,8533,8534,8537],{},[270,8535,8536],{},"团队多人多 Jira 实例",[270,8538,8539,8540,8543],{},"多 Profile 管理，",[45,8541,8542],{},"--user=work"," 切换",[255,8545,8546,8549],{},[270,8547,8548],{},"Windows 终端体验差",[270,8550,8551,8552,8555],{},"原生 ",[45,8553,8554],{},".exe"," 构建 + 一键安装脚本",[255,8557,8558,8561],{},[270,8559,8560],{},"AI 生成 JQL 困难",[270,8562,8563,8564],{},"字段元数据发现：",[45,8565,8566],{},"field list --query xxx",[255,8568,8569,8572],{},[270,8570,8571],{},"MCP 直接拿原始 JSON 太冗长",[270,8573,8574,8575,8578],{},"紧凑视图 + ",[45,8576,8577],{},"--view summary"," 减少 token 消耗",[11,8580,6794],{"id":6794},[75,8582,8584],{"className":1291,"code":8583,"language":1293,"meta":81,"style":81},"# 从源码安装（要求 Go 1.26.3+）\ngo install github.com\u002Fyourname\u002Fjira_cli@latest\n\n# 或下载预编译二进制\n# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[45,8585,8586,8591,8600,8604,8609],{"__ignoreMap":81},[213,8587,8588],{"class":215,"line":216},[213,8589,8590],{"class":219},"# 从源码安装（要求 Go 1.26.3+）\n",[213,8592,8593,8595,8597],{"class":215,"line":223},[213,8594,209],{"class":237},[213,8596,1363],{"class":377},[213,8598,8599],{"class":377}," github.com\u002Fyourname\u002Fjira_cli@latest\n",[213,8601,8602],{"class":215,"line":244},[213,8603,667],{"emptyLinePlaceholder":666},[213,8605,8606],{"class":215,"line":360},[213,8607,8608],{"class":219},"# 或下载预编译二进制\n",[213,8610,8611],{"class":215,"line":395},[213,8612,8613],{"class":219},"# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[15,8615,8616,8617,8620],{},"Windows 用户下载 zip 包解压后双击 ",[45,8618,8619],{},"install.bat"," 即可全局安装，自动加入 PATH。",[11,8622,8623],{"id":8623},"快速上手",[113,8625,8627],{"id":8626},"_1-配置认证","1. 配置认证",[75,8629,8631],{"className":1291,"code":8630,"language":1293,"meta":81,"style":81},"# 设置 Jira 地址\njira_cli set base-url https:\u002F\u002Fjira.your-company.com --global\n\n# Token 认证（推荐）\njira_cli set token \u003Cyour-token> --global\n\n# 或用户名密码认证\njira_cli set auth \u003Cusername> \u003Cpassword> --global\n\n# 验证一下\njira_cli user me\n",[45,8632,8633,8638,8654,8658,8663,8683,8687,8692,8721,8725,8730],{"__ignoreMap":81},[213,8634,8635],{"class":215,"line":216},[213,8636,8637],{"class":219},"# 设置 Jira 地址\n",[213,8639,8640,8642,8645,8648,8651],{"class":215,"line":223},[213,8641,8420],{"class":237},[213,8643,8644],{"class":377}," set",[213,8646,8647],{"class":377}," base-url",[213,8649,8650],{"class":377}," https:\u002F\u002Fjira.your-company.com",[213,8652,8653],{"class":339}," --global\n",[213,8655,8656],{"class":215,"line":244},[213,8657,667],{"emptyLinePlaceholder":666},[213,8659,8660],{"class":215,"line":360},[213,8661,8662],{"class":219},"# Token 认证（推荐）\n",[213,8664,8665,8667,8669,8672,8674,8677,8679,8681],{"class":215,"line":395},[213,8666,8420],{"class":237},[213,8668,8644],{"class":377},[213,8670,8671],{"class":377}," token",[213,8673,1808],{"class":230},[213,8675,8676],{"class":377},"your-toke",[213,8678,2212],{"class":226},[213,8680,1817],{"class":230},[213,8682,8653],{"class":339},[213,8684,8685],{"class":215,"line":412},[213,8686,667],{"emptyLinePlaceholder":666},[213,8688,8689],{"class":215,"line":430},[213,8690,8691],{"class":219},"# 或用户名密码认证\n",[213,8693,8694,8696,8698,8700,8702,8705,8707,8709,8711,8714,8717,8719],{"class":215,"line":442},[213,8695,8420],{"class":237},[213,8697,8644],{"class":377},[213,8699,6868],{"class":377},[213,8701,1808],{"class":230},[213,8703,8704],{"class":377},"usernam",[213,8706,2198],{"class":226},[213,8708,1817],{"class":230},[213,8710,1808],{"class":230},[213,8712,8713],{"class":377},"passwor",[213,8715,8716],{"class":226},"d",[213,8718,1817],{"class":230},[213,8720,8653],{"class":339},[213,8722,8723],{"class":215,"line":448},[213,8724,667],{"emptyLinePlaceholder":666},[213,8726,8727],{"class":215,"line":461},[213,8728,8729],{"class":219},"# 验证一下\n",[213,8731,8732,8734,8737],{"class":215,"line":751},[213,8733,8420],{"class":237},[213,8735,8736],{"class":377}," user",[213,8738,8739],{"class":377}," me\n",[113,8741,8743],{"id":8742},"_2-日常使用","2. 日常使用",[75,8745,8747],{"className":1291,"code":8746,"language":1293,"meta":81,"style":81},"# 查自己今天的任务\njira_cli issue search \"assignee = currentUser() ORDER BY updated DESC\"\n\n# 看某个任务详情\njira_cli issue get PROJ-123\n\n# 流转状态\njira_cli issue transitions PROJ-123                    # 可以转哪些\njira_cli issue do-transition PROJ-123 21               # 执行流转\n\n# 添加备注\njira_cli issue comment PROJ-123 \"已定位到问题，正在修复\"\n\n# 创建任务\njira_cli issue create PROJ Task --fields @fields.json\n",[45,8748,8749,8754,8765,8769,8774,8784,8788,8793,8807,8824,8828,8833,8846,8850,8855],{"__ignoreMap":81},[213,8750,8751],{"class":215,"line":216},[213,8752,8753],{"class":219},"# 查自己今天的任务\n",[213,8755,8756,8758,8760,8762],{"class":215,"line":223},[213,8757,8420],{"class":237},[213,8759,8445],{"class":377},[213,8761,8448],{"class":377},[213,8763,8764],{"class":377}," \"assignee = currentUser() ORDER BY updated DESC\"\n",[213,8766,8767],{"class":215,"line":244},[213,8768,667],{"emptyLinePlaceholder":666},[213,8770,8771],{"class":215,"line":360},[213,8772,8773],{"class":219},"# 看某个任务详情\n",[213,8775,8776,8778,8780,8782],{"class":215,"line":395},[213,8777,8420],{"class":237},[213,8779,8445],{"class":377},[213,8781,4668],{"class":377},[213,8783,8462],{"class":377},[213,8785,8786],{"class":215,"line":412},[213,8787,667],{"emptyLinePlaceholder":666},[213,8789,8790],{"class":215,"line":430},[213,8791,8792],{"class":219},"# 流转状态\n",[213,8794,8795,8797,8799,8802,8804],{"class":215,"line":442},[213,8796,8420],{"class":237},[213,8798,8445],{"class":377},[213,8800,8801],{"class":377}," transitions",[213,8803,8474],{"class":377},[213,8805,8806],{"class":219},"                    # 可以转哪些\n",[213,8808,8809,8811,8813,8816,8818,8821],{"class":215,"line":448},[213,8810,8420],{"class":237},[213,8812,8445],{"class":377},[213,8814,8815],{"class":377}," do-transition",[213,8817,8474],{"class":377},[213,8819,8820],{"class":339}," 21",[213,8822,8823],{"class":219},"               # 执行流转\n",[213,8825,8826],{"class":215,"line":461},[213,8827,667],{"emptyLinePlaceholder":666},[213,8829,8830],{"class":215,"line":751},[213,8831,8832],{"class":219},"# 添加备注\n",[213,8834,8835,8837,8839,8841,8843],{"class":215,"line":765},[213,8836,8420],{"class":237},[213,8838,8445],{"class":377},[213,8840,8471],{"class":377},[213,8842,8474],{"class":377},[213,8844,8845],{"class":377}," \"已定位到问题，正在修复\"\n",[213,8847,8848],{"class":215,"line":774},[213,8849,667],{"emptyLinePlaceholder":666},[213,8851,8852],{"class":215,"line":780},[213,8853,8854],{"class":219},"# 创建任务\n",[213,8856,8857,8859,8861,8863,8866,8869,8872],{"class":215,"line":797},[213,8858,8420],{"class":237},[213,8860,8445],{"class":377},[213,8862,6754],{"class":377},[213,8864,8865],{"class":377}," PROJ",[213,8867,8868],{"class":377}," Task",[213,8870,8871],{"class":339}," --fields",[213,8873,8874],{"class":377}," @fields.json\n",[113,8876,8878],{"id":8877},"_3-配给-ai-使用","3. 配给 AI 使用",[75,8880,8882],{"className":1291,"code":8881,"language":1293,"meta":81,"style":81},"# 先配置一个只读账号给 AI 用\njira_cli set base-url https:\u002F\u002Fjira.your-company.com --name ai --global\njira_cli set token \u003Ctoken> --name ai --global\njira_cli set readonly on --name ai --global\n",[45,8883,8884,8889,8906,8929],{"__ignoreMap":81},[213,8885,8886],{"class":215,"line":216},[213,8887,8888],{"class":219},"# 先配置一个只读账号给 AI 用\n",[213,8890,8891,8893,8895,8897,8899,8901,8904],{"class":215,"line":223},[213,8892,8420],{"class":237},[213,8894,8644],{"class":377},[213,8896,8647],{"class":377},[213,8898,8650],{"class":377},[213,8900,1805],{"class":339},[213,8902,8903],{"class":377}," ai",[213,8905,8653],{"class":339},[213,8907,8908,8910,8912,8914,8916,8919,8921,8923,8925,8927],{"class":215,"line":244},[213,8909,8420],{"class":237},[213,8911,8644],{"class":377},[213,8913,8671],{"class":377},[213,8915,1808],{"class":230},[213,8917,8918],{"class":377},"toke",[213,8920,2212],{"class":226},[213,8922,1817],{"class":230},[213,8924,1805],{"class":339},[213,8926,8903],{"class":377},[213,8928,8653],{"class":339},[213,8930,8931,8933,8935,8938,8941,8943,8945],{"class":215,"line":360},[213,8932,8420],{"class":237},[213,8934,8644],{"class":377},[213,8936,8937],{"class":377}," readonly",[213,8939,8940],{"class":377}," on",[213,8942,1805],{"class":339},[213,8944,8903],{"class":377},[213,8946,8653],{"class":339},[15,8948,8949],{},"然后在 AI 工具的 MCP 配置里加上：",[75,8951,8953],{"className":6465,"code":8952,"language":6467,"meta":81,"style":81},"{\n  \"mcpServers\": {\n    \"jira\": {\n      \"command\": \"jira_cli\",\n      \"args\": [\"mcp\", \"--user=ai\"]\n    }\n  }\n}\n",[45,8954,8955,8959,8965,8972,8984,9001,9005,9009],{"__ignoreMap":81},[213,8956,8957],{"class":215,"line":216},[213,8958,6474],{"class":226},[213,8960,8961,8963],{"class":215,"line":223},[213,8962,6479],{"class":339},[213,8964,3195],{"class":226},[213,8966,8967,8970],{"class":215,"line":244},[213,8968,8969],{"class":339},"    \"jira\"",[213,8971,3195],{"class":226},[213,8973,8974,8977,8979,8982],{"class":215,"line":360},[213,8975,8976],{"class":339},"      \"command\"",[213,8978,2981],{"class":226},[213,8980,8981],{"class":377},"\"jira_cli\"",[213,8983,2928],{"class":226},[213,8985,8986,8989,8991,8994,8996,8999],{"class":215,"line":395},[213,8987,8988],{"class":339},"      \"args\"",[213,8990,3012],{"class":226},[213,8992,8993],{"class":377},"\"mcp\"",[213,8995,381],{"class":226},[213,8997,8998],{"class":377},"\"--user=ai\"",[213,9000,703],{"class":226},[213,9002,9003],{"class":215,"line":412},[213,9004,777],{"class":226},[213,9006,9007],{"class":215,"line":430},[213,9008,3239],{"class":226},[213,9010,9011],{"class":215,"line":442},[213,9012,445],{"class":226},[15,9014,9015],{},"这样 AI 就能查 Jira 但不能修改，既有了上下文，又不用担心误操作。",[11,9017,7276],{"id":7276},[113,9019,9021],{"id":9020},"_1-账号级只读策略","1. 账号级只读策略",[15,9023,9024],{},"这是 jira_cli 最实用的设计。给 AI Agent 配置只读账号：",[75,9026,9028],{"className":1291,"code":9027,"language":1293,"meta":81,"style":81},"jira_cli set readonly on --name ai\n",[45,9029,9030],{"__ignoreMap":81},[213,9031,9032,9034,9036,9038,9040,9042],{"class":215,"line":216},[213,9033,8420],{"class":237},[213,9035,8644],{"class":377},[213,9037,8937],{"class":377},[213,9039,8940],{"class":377},[213,9041,1805],{"class":339},[213,9043,9044],{"class":377}," ai\n",[15,9046,9047],{},"只读模式下：",[177,9049,9050,9053,9056],{},[29,9051,9052],{},"CLI 写命令被隐藏和拒绝",[29,9054,9055],{},"MCP 的写入工具不会注册",[29,9057,9058,9059,9062],{},"环境变量 ",[45,9060,9061],{},"JIRA_READONLY=true"," 也可以临时覆盖",[15,9064,9065],{},"AI 只能查 JQL、看任务、查字段、读评论，绝对不会误改数据。需要创建\u002F更新时，另配一个有写入权限的账号由开发者自己操作。",[113,9067,9069],{"id":9068},"_2-多-profile-切换","2. 多 Profile 切换",[15,9071,9072,9073,9075],{},"一个配置文件里管理多个 Jira 账号，",[45,9074,2163],{}," 切换：",[75,9077,9079],{"className":1291,"code":9078,"language":1293,"meta":81,"style":81},"# 工作账号\njira_cli set base-url https:\u002F\u002Fjira.company.com --name work --global\njira_cli set token \u003Cwork-token> --name work --global\n\n# 个人账号\njira_cli set base-url https:\u002F\u002Fjira.personal.com --name personal --global\njira_cli set token \u003Cpersonal-token> --name personal --global\n\n# 使用时切换\njira_cli --user=work issue search \"project = PROJ\"\njira_cli --user=personal issue search \"project = SIDE\"\n",[45,9080,9081,9086,9104,9127,9131,9136,9154,9177,9181,9186,9200],{"__ignoreMap":81},[213,9082,9083],{"class":215,"line":216},[213,9084,9085],{"class":219},"# 工作账号\n",[213,9087,9088,9090,9092,9094,9097,9099,9102],{"class":215,"line":223},[213,9089,8420],{"class":237},[213,9091,8644],{"class":377},[213,9093,8647],{"class":377},[213,9095,9096],{"class":377}," https:\u002F\u002Fjira.company.com",[213,9098,1805],{"class":339},[213,9100,9101],{"class":377}," work",[213,9103,8653],{"class":339},[213,9105,9106,9108,9110,9112,9114,9117,9119,9121,9123,9125],{"class":215,"line":244},[213,9107,8420],{"class":237},[213,9109,8644],{"class":377},[213,9111,8671],{"class":377},[213,9113,1808],{"class":230},[213,9115,9116],{"class":377},"work-toke",[213,9118,2212],{"class":226},[213,9120,1817],{"class":230},[213,9122,1805],{"class":339},[213,9124,9101],{"class":377},[213,9126,8653],{"class":339},[213,9128,9129],{"class":215,"line":360},[213,9130,667],{"emptyLinePlaceholder":666},[213,9132,9133],{"class":215,"line":395},[213,9134,9135],{"class":219},"# 个人账号\n",[213,9137,9138,9140,9142,9144,9147,9149,9152],{"class":215,"line":412},[213,9139,8420],{"class":237},[213,9141,8644],{"class":377},[213,9143,8647],{"class":377},[213,9145,9146],{"class":377}," https:\u002F\u002Fjira.personal.com",[213,9148,1805],{"class":339},[213,9150,9151],{"class":377}," personal",[213,9153,8653],{"class":339},[213,9155,9156,9158,9160,9162,9164,9167,9169,9171,9173,9175],{"class":215,"line":430},[213,9157,8420],{"class":237},[213,9159,8644],{"class":377},[213,9161,8671],{"class":377},[213,9163,1808],{"class":230},[213,9165,9166],{"class":377},"personal-toke",[213,9168,2212],{"class":226},[213,9170,1817],{"class":230},[213,9172,1805],{"class":339},[213,9174,9151],{"class":377},[213,9176,8653],{"class":339},[213,9178,9179],{"class":215,"line":442},[213,9180,667],{"emptyLinePlaceholder":666},[213,9182,9183],{"class":215,"line":448},[213,9184,9185],{"class":219},"# 使用时切换\n",[213,9187,9188,9190,9193,9195,9197],{"class":215,"line":461},[213,9189,8420],{"class":237},[213,9191,9192],{"class":339}," --user=work",[213,9194,8445],{"class":377},[213,9196,8448],{"class":377},[213,9198,9199],{"class":377}," \"project = PROJ\"\n",[213,9201,9202,9204,9207,9209,9211],{"class":215,"line":751},[213,9203,8420],{"class":237},[213,9205,9206],{"class":339}," --user=personal",[213,9208,8445],{"class":377},[213,9210,8448],{"class":377},[213,9212,9213],{"class":377}," \"project = SIDE\"\n",[15,9215,9216,9217,9220,9221,9224],{},"同时支持",[19,9218,9219],{},"项目级配置","（",[45,9222,9223],{},"--local","）和工作目录绑定：",[75,9226,9228],{"className":1291,"code":9227,"language":1293,"meta":81,"style":81},"# 在项目目录下，配置只对当前项目生效\ncd ~\u002Fprojects\u002Fbackend\njira_cli set base-url https:\u002F\u002Fjira-backend.company.com --local\n",[45,9229,9230,9235,9242],{"__ignoreMap":81},[213,9231,9232],{"class":215,"line":216},[213,9233,9234],{"class":219},"# 在项目目录下，配置只对当前项目生效\n",[213,9236,9237,9239],{"class":215,"line":223},[213,9238,8015],{"class":339},[213,9240,9241],{"class":377}," ~\u002Fprojects\u002Fbackend\n",[213,9243,9244,9246,9248,9250,9253],{"class":215,"line":244},[213,9245,8420],{"class":237},[213,9247,8644],{"class":377},[213,9249,8647],{"class":377},[213,9251,9252],{"class":377}," https:\u002F\u002Fjira-backend.company.com",[213,9254,9255],{"class":339}," --local\n",[113,9257,9259],{"id":9258},"_3-对-ai-友好的输出","3. 对 AI 友好的输出",[15,9261,9262],{},"jira_cli 在输出设计上充分考虑了 AI Agent 的 token 消耗：",[75,9264,9266],{"className":1291,"code":9265,"language":1293,"meta":81,"style":81},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\njira_cli issue get PROJ-123 --view summary\n\n# 表格输出 — 一眼看到关键信息\njira_cli issue search \"project = PROJ\" --output table\n\n# JSON 输出 — 程序解析，不转义 Unicode\njira_cli --output json issue get PROJ-123\n\n# 自定义搜索列 — 只要需要的字段\njira_cli set search-columns key,summary,fixVersions\n",[45,9267,9268,9273,9289,9293,9298,9315,9319,9324,9339,9343,9348],{"__ignoreMap":81},[213,9269,9270],{"class":215,"line":216},[213,9271,9272],{"class":219},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\n",[213,9274,9275,9277,9279,9281,9283,9286],{"class":215,"line":223},[213,9276,8420],{"class":237},[213,9278,8445],{"class":377},[213,9280,4668],{"class":377},[213,9282,8474],{"class":377},[213,9284,9285],{"class":339}," --view",[213,9287,9288],{"class":377}," summary\n",[213,9290,9291],{"class":215,"line":244},[213,9292,667],{"emptyLinePlaceholder":666},[213,9294,9295],{"class":215,"line":360},[213,9296,9297],{"class":219},"# 表格输出 — 一眼看到关键信息\n",[213,9299,9300,9302,9304,9306,9309,9312],{"class":215,"line":395},[213,9301,8420],{"class":237},[213,9303,8445],{"class":377},[213,9305,8448],{"class":377},[213,9307,9308],{"class":377}," \"project = PROJ\"",[213,9310,9311],{"class":339}," --output",[213,9313,9314],{"class":377}," table\n",[213,9316,9317],{"class":215,"line":412},[213,9318,667],{"emptyLinePlaceholder":666},[213,9320,9321],{"class":215,"line":430},[213,9322,9323],{"class":219},"# JSON 输出 — 程序解析，不转义 Unicode\n",[213,9325,9326,9328,9330,9333,9335,9337],{"class":215,"line":442},[213,9327,8420],{"class":237},[213,9329,9311],{"class":339},[213,9331,9332],{"class":377}," json",[213,9334,8445],{"class":377},[213,9336,4668],{"class":377},[213,9338,8462],{"class":377},[213,9340,9341],{"class":215,"line":448},[213,9342,667],{"emptyLinePlaceholder":666},[213,9344,9345],{"class":215,"line":461},[213,9346,9347],{"class":219},"# 自定义搜索列 — 只要需要的字段\n",[213,9349,9350,9352,9354,9357],{"class":215,"line":751},[213,9351,8420],{"class":237},[213,9353,8644],{"class":377},[213,9355,9356],{"class":377}," search-columns",[213,9358,9359],{"class":377}," key,summary,fixVersions\n",[113,9361,9363],{"id":9362},"_4-字段元数据发现","4. 字段元数据发现",[15,9365,9366],{},"Jira 的 customfield 让人头疼，jira_cli 提供了完整的字段发现能力：",[75,9368,9370],{"className":1291,"code":9369,"language":1293,"meta":81,"style":81},"# 列出所有字段及其类型\njira_cli field list\n\n# 搜索包含特定关键词的字段\njira_cli field list --query \"版本\"\n\n# 只看自定义字段\njira_cli field list --custom-only\n\n# 创建任务前，先看看需要哪些字段\njira_cli issue create-meta PROJ --issue-type Task\n\n# 分页查看可选值\njira_cli issue create-meta-values PROJ Task priority --query High\n",[45,9371,9372,9377,9386,9390,9395,9409,9413,9418,9429,9433,9438,9455,9459,9464],{"__ignoreMap":81},[213,9373,9374],{"class":215,"line":216},[213,9375,9376],{"class":219},"# 列出所有字段及其类型\n",[213,9378,9379,9381,9384],{"class":215,"line":223},[213,9380,8420],{"class":237},[213,9382,9383],{"class":377}," field",[213,9385,3326],{"class":377},[213,9387,9388],{"class":215,"line":244},[213,9389,667],{"emptyLinePlaceholder":666},[213,9391,9392],{"class":215,"line":360},[213,9393,9394],{"class":219},"# 搜索包含特定关键词的字段\n",[213,9396,9397,9399,9401,9403,9406],{"class":215,"line":395},[213,9398,8420],{"class":237},[213,9400,9383],{"class":377},[213,9402,6689],{"class":377},[213,9404,9405],{"class":339}," --query",[213,9407,9408],{"class":377}," \"版本\"\n",[213,9410,9411],{"class":215,"line":412},[213,9412,667],{"emptyLinePlaceholder":666},[213,9414,9415],{"class":215,"line":430},[213,9416,9417],{"class":219},"# 只看自定义字段\n",[213,9419,9420,9422,9424,9426],{"class":215,"line":442},[213,9421,8420],{"class":237},[213,9423,9383],{"class":377},[213,9425,6689],{"class":377},[213,9427,9428],{"class":339}," --custom-only\n",[213,9430,9431],{"class":215,"line":448},[213,9432,667],{"emptyLinePlaceholder":666},[213,9434,9435],{"class":215,"line":461},[213,9436,9437],{"class":219},"# 创建任务前，先看看需要哪些字段\n",[213,9439,9440,9442,9444,9447,9449,9452],{"class":215,"line":751},[213,9441,8420],{"class":237},[213,9443,8445],{"class":377},[213,9445,9446],{"class":377}," create-meta",[213,9448,8865],{"class":377},[213,9450,9451],{"class":339}," --issue-type",[213,9453,9454],{"class":377}," Task\n",[213,9456,9457],{"class":215,"line":765},[213,9458,667],{"emptyLinePlaceholder":666},[213,9460,9461],{"class":215,"line":774},[213,9462,9463],{"class":219},"# 分页查看可选值\n",[213,9465,9466,9468,9470,9473,9475,9477,9480,9482],{"class":215,"line":780},[213,9467,8420],{"class":237},[213,9469,8445],{"class":377},[213,9471,9472],{"class":377}," create-meta-values",[213,9474,8865],{"class":377},[213,9476,8868],{"class":377},[213,9478,9479],{"class":377}," priority",[213,9481,9405],{"class":339},[213,9483,9484],{"class":377}," High\n",[113,9486,9488],{"id":9487},"_5-mcp-写入控制","5. MCP 写入控制",[15,9490,9491],{},"除了全局只读，MCP 还有更细粒度的控制：",[249,9493,9494,9504],{},[252,9495,9496],{},[255,9497,9498,9501],{},[258,9499,9500],{},"环境变量",[258,9502,9503],{},"作用",[265,9505,9506,9516,9526,9536,9546],{},[255,9507,9508,9513],{},[270,9509,9510],{},[45,9511,9512],{},"JIRA_CREATE_OPEN=false",[270,9514,9515],{},"禁用创建\u002F更新工具",[255,9517,9518,9523],{},[270,9519,9520],{},[45,9521,9522],{},"JIRA_DISABLE_DISCLAIMER=true",[270,9524,9525],{},"去掉 AI 自动添加的备注声明",[255,9527,9528,9533],{},[270,9529,9530],{},[45,9531,9532],{},"JIRA_CREATE_SUMMARY_MAX",[270,9534,9535],{},"限制摘要长度",[255,9537,9538,9543],{},[270,9539,9540],{},[45,9541,9542],{},"JIRA_CREATE_DESCRIPTION_MAX",[270,9544,9545],{},"限制描述长度",[255,9547,9548,9553],{},[270,9549,9550],{},[45,9551,9552],{},"JIRA_CREATE_FIELDS_MAX_BYTES",[270,9554,9555],{},"限制创建字段的 JSON 大小",[15,9557,9558],{},"这些限制确保 AI 即使有写入权限也不会\"失控\"——例如防止 AI 往描述里塞几万字的废话。",[113,9560,9562],{"id":9561},"_6-中英双语帮助","6. 中英双语帮助",[15,9564,9565],{},"帮助信息默认中文，也支持一键切英文：",[75,9567,9569],{"className":1291,"code":9568,"language":1293,"meta":81,"style":81},"# 保存英文偏好\njira_cli set help-language en\n\n# 切回中文\njira_cli set help-language zh\n\n# 临时覆盖\njira_cli --lang en issue search -h\n",[45,9570,9571,9576,9588,9592,9597,9608,9612,9617],{"__ignoreMap":81},[213,9572,9573],{"class":215,"line":216},[213,9574,9575],{"class":219},"# 保存英文偏好\n",[213,9577,9578,9580,9582,9585],{"class":215,"line":223},[213,9579,8420],{"class":237},[213,9581,8644],{"class":377},[213,9583,9584],{"class":377}," help-language",[213,9586,9587],{"class":377}," en\n",[213,9589,9590],{"class":215,"line":244},[213,9591,667],{"emptyLinePlaceholder":666},[213,9593,9594],{"class":215,"line":360},[213,9595,9596],{"class":219},"# 切回中文\n",[213,9598,9599,9601,9603,9605],{"class":215,"line":395},[213,9600,8420],{"class":237},[213,9602,8644],{"class":377},[213,9604,9584],{"class":377},[213,9606,9607],{"class":377}," zh\n",[213,9609,9610],{"class":215,"line":412},[213,9611,667],{"emptyLinePlaceholder":666},[213,9613,9614],{"class":215,"line":430},[213,9615,9616],{"class":219},"# 临时覆盖\n",[213,9618,9619,9621,9624,9627,9629,9631],{"class":215,"line":442},[213,9620,8420],{"class":237},[213,9622,9623],{"class":339}," --lang",[213,9625,9626],{"class":377}," en",[213,9628,8445],{"class":377},[213,9630,8448],{"class":377},[213,9632,9633],{"class":339}," -h\n",[11,9635,9636],{"id":9636},"架构一览",[75,9638,9641],{"className":9639,"code":9640,"language":80},[78],"main.go\n  cmd\u002F                         CLI 命令、MCP 启动、访问策略\n  internal\u002Fconfig\u002F             运行时配置、Profile 持久化\n  internal\u002Ftools\u002Fjira_tools\u002F   MCP 输入\u002F输出适配层\n  pkg\u002Fjira\u002F                    可复用的 Jira REST 客户端\n",[45,9642,9640],{"__ignoreMap":81},[15,9644,9645],{},"设计原则清晰：",[177,9647,9648,9656,9664,9672],{},[29,9649,9650,9655],{},[19,9651,9652],{},[45,9653,9654],{},"pkg\u002Fjira"," 是纯 Jira 客户端，HTTP、认证、上下文处理都在这里",[29,9657,9658,9663],{},[19,9659,9660],{},[45,9661,9662],{},"cmd"," 只管 Cobra 解析和呈现",[29,9665,9666,9671],{},[19,9667,9668],{},[45,9669,9670],{},"internal\u002Ftools\u002Fjira_tools"," 只管 MCP schema 验证和结果适配",[29,9673,9674,9679],{},[19,9675,9676],{},[45,9677,9678],{},"internal\u002Fconfig"," 拥有 Profile 的全部逻辑，不分散到 cmd",[15,9681,9682,9683,9686],{},"每一层职责单一，新增 Jira 能力时按 ",[45,9684,9685],{},"endpoints.go → Client 方法 → CLI 命令 → MCP tool → 策略注册"," 的路径走就行。",[11,9688,9689],{"id":9689},"和已有方案的对比",[249,9691,9692,9704],{},[252,9693,9694],{},[255,9695,9696,9698,9701],{},[258,9697,3946],{},[258,9699,9700],{},"适合场景",[258,9702,9703],{},"jira_cli 的区别",[265,9705,9706,9717,9728,9739],{},[255,9707,9708,9711,9714],{},[270,9709,9710],{},"Atlassian Rovo MCP Server",[270,9712,9713],{},"Jira Cloud、官方 OAuth",[270,9715,9716],{},"jira_cli 是本地 stdio，适合内网 Server\u002FDC",[255,9718,9719,9722,9725],{},[270,9720,9721],{},"go-jira \u002F jira-cli 社区版",[270,9723,9724],{},"纯命令行管理",[270,9726,9727],{},"jira_cli 同时提供 MCP server，面向 AI 工作流",[255,9729,9730,9733,9736],{},[270,9731,9732],{},"直接调 REST API",[270,9734,9735],{},"自定义集成",[270,9737,9738],{},"jira_cli 封装了认证、只读策略、字段元数据",[255,9740,9741,9744,9747],{},[270,9742,9743],{},"Jira 网页",[270,9745,9746],{},"日常使用",[270,9748,9749],{},"jira_cli 把 Jira 变成终端里的一行命令",[11,9751,2439],{"id":2439},[15,9753,9754],{},"如果你的团队满足以下任一条件，jira_cli 值得一试：",[177,9756,9757,9760,9763,9766,9769],{},[29,9758,9759],{},"企业 Jira 部署在内网，想接入 AI 编程工具",[29,9761,9762],{},"想给 AI Agent 只读权限来查 Jira 上下文",[29,9764,9765],{},"需要命令行快速查看\u002F操作 Jira，不想切浏览器",[29,9767,9768],{},"需要管理多个 Jira 实例和多套账号",[29,9770,9771],{},"Windows 用户想要无摩擦的 CLI 体验",[15,9773,9774,9775,9779],{},"GitHub：",[3097,9776,9778],{"href":8418,"rel":9777},[3101],"github.com\u002Fyourname\u002Fjira_cli"," | MIT 协议 | Go 1.26.3+",[1231,9781,9782],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}",{"title":81,"searchDepth":223,"depth":244,"links":9784},[9785,9786,9787,9788,9793,9801,9802,9803],{"id":8412,"depth":223,"text":8413},{"id":8498,"depth":223,"text":8498},{"id":6794,"depth":223,"text":6794},{"id":8623,"depth":223,"text":8623,"children":9789},[9790,9791,9792],{"id":8626,"depth":244,"text":8627},{"id":8742,"depth":244,"text":8743},{"id":8877,"depth":244,"text":8878},{"id":7276,"depth":223,"text":7276,"children":9794},[9795,9796,9797,9798,9799,9800],{"id":9020,"depth":244,"text":9021},{"id":9068,"depth":244,"text":9069},{"id":9258,"depth":244,"text":9259},{"id":9362,"depth":244,"text":9363},{"id":9487,"depth":244,"text":9488},{"id":9561,"depth":244,"text":9562},{"id":9636,"depth":223,"text":9636},{"id":9689,"depth":223,"text":9689},{"id":2439,"depth":223,"text":2439},"介绍 jira_cli，一个开源的企业 Jira CLI 命令行工具和MCP，支持 Jira Server \u002F Data Center，自带 MCP stdio server，让 Claude Code、Codex 等 AI 编程工具直接读写 Jira 任务。","\u002Fposts\u002Fjira-cli-intro\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fjira-cli-intro","2026-07-17",{"title":8407,"description":9804},"posts\u002Fjira-cli-intro\u002Findex",[9812,7426,6641,2076,6643,7427],"Jira","vNH6sF2mMFmSjbHoEI2OqmYCgpFmaqyKt-MUfcQqXrU",{"id":9815,"title":9816,"author":6,"body":9817,"description":10360,"difficulty":1259,"draft":1260,"extension":1261,"image":10361,"meta":10362,"navigation":666,"path":10363,"pinned":1260,"published":10364,"seo":10365,"series":10366,"seriesOrder":216,"stem":10367,"tags":10368,"updated":10371,"__hash__":10372},"posts\u002Fposts\u002Fsublog-evolution\u002Findex.md","尼克的小窝 架构变迁：从纯静态到前后端分离",{"type":8,"value":9818,"toc":10340},[9819,9822,9825,9836,9840,9844,9850,9854,9885,9888,9902,9906,9909,9977,9981,9984,9990,9994,10004,10034,10037,10041,10049,10052,10096,10100,10221,10225,10232,10236,10242,10248,10252,10255,10306,10310,10317,10324,10330,10333,10335],[11,9820,9821],{"id":13},"📝 写在前面",[15,9823,9824],{},"SuBlog 最初只是一个简单的技术博客——Nuxt 3 静态生成，Markdown 写文章，部署到静态托管完事。但随着功能需求的增长，纯静态的架构开始显露出局限性。",[15,9826,9827,9828,9831,9832,9835],{},"这篇文章记录从 ",[19,9829,9830],{},"纯静态"," 到 ",[19,9833,9834],{},"前后端分离"," 的演进过程，带你了解每一步背后的思考。",[11,9837,9839],{"id":9838},"第一代纯静态nuxt-3-nuxtcontent","📦 第一代：纯静态（Nuxt 3 + @nuxt\u002Fcontent）",[113,9841,9843],{"id":9842},"️-架构图","🏗️ 架构图",[75,9845,9848],{"className":9846,"code":9847,"language":80,"meta":81},[78],"┌─────────────────────────────────────┐\n│          静态托管 (Nginx\u002FVercel)      │\n│                                     │\n│  ┌───────────────────────────────┐  │\n│  │     Nuxt 3 SSG 产物            │  │\n│  │                               │  │\n│  │  pages\u002F + content\u002F + public\u002F  │  │\n│  └───────────────────────────────┘  │\n│                                     │\n│  Giscus (GitHub Discussions 评论)    │\n└─────────────────────────────────────┘\n",[45,9849,9847],{"__ignoreMap":81},[113,9851,9853],{"id":9852},"做了什么","🔧 做了什么",[177,9855,9856,9865,9870,9875],{},[29,9857,9858,2456,9860,2456,9862,9864],{},[19,9859,2765],{},[19,9861,2775],{},[19,9863,2785],{}," 搭建前端",[29,9866,9867,9869],{},[19,9868,2795],{}," 管理 Markdown 文章，编译时生成静态页面",[29,9871,9872,9874],{},[19,9873,2815],{}," 嵌入评论，依赖 GitHub Discussions",[29,9876,9877,9878,9881,9882,9884],{},"部署通过 ",[45,9879,9880],{},"nuxt generate"," 输出 ",[45,9883,3054],{},"，丢到 Nginx 或 Vercel",[113,9886,9887],{"id":260},"✅ 优点",[177,9889,9890,9896,9899],{},[29,9891,9892,9893,9895],{},"部署极简，一个 ",[45,9894,9880],{}," 搞定",[29,9897,9898],{},"加载飞快，全是 HTML，没有服务端开销",[29,9900,9901],{},"无需后端，零运维成本",[113,9903,9905],{"id":9904},"局限","❌ 局限",[15,9907,9908],{},"随着时间的推移，纯静态的问题越来越明显：",[249,9910,9911,9920],{},[252,9912,9913],{},[255,9914,9915,9917],{},[258,9916,4240],{},[258,9918,9919],{},"具体表现",[265,9921,9922,9933,9944,9955,9966],{},[255,9923,9924,9930],{},[270,9925,9926,9927],{},"🐢 ",[19,9928,9929],{},"内容更新慢",[270,9931,9932],{},"改一篇文章要重新构建、重新部署，不能实时发布",[255,9934,9935,9941],{},[270,9936,9937,9938],{},"👤 ",[19,9939,9940],{},"没有用户体系",[270,9942,9943],{},"无法区分读者和管理员，所有内容对所有人一致可见",[255,9945,9946,9952],{},[270,9947,9948,9949],{},"🔒 ",[19,9950,9951],{},"评论受限",[270,9953,9954],{},"Giscus 需要 GitHub 账号才能评论，门槛高",[255,9956,9957,9963],{},[270,9958,9959,9960],{},"🚫 ",[19,9961,9962],{},"无法做动态功能",[270,9964,9965],{},"点赞、收藏、阅读统计、个性化推荐……统统做不了",[255,9967,9968,9974],{},[270,9969,9970,9971],{},"🔗 ",[19,9972,9973],{},"前端锁死",[270,9975,9976],{},"被 Nuxt\u002FVue 生态绑定，想换技术栈得重写",[11,9978,9980],{"id":9979},"第二代前后端分离react-go","⚡ 第二代：前后端分离（React + Go）",[113,9982,9843],{"id":9983},"️-架构图-1",[75,9985,9988],{"className":9986,"code":9987,"language":80,"meta":81},[78],"┌───────────────────────┐       ┌────────────────────┐\n│    静态托管 (Vercel)    │       │   Go 服务端         │\n│                       │       │                    │\n│  ┌─────────────────┐  │       │  ┌──────────────┐  │\n│  │   sublog-react   │  │  API  │  │  HTTP API    │  │\n│  │  (Next.js SSG)  │──┼───────┼─>│              │  │\n│  │                  │  │       │  │ 文章 \u002F 用户  │  │\n│  │  首页 \u002F 文章 \u002F   │  │       │  └──────────────┘  │\n│  │  用户中心        │  │       │                    │\n│  └─────────────────┘  │       │  ┌──────────────┐  │\n│                       │       │  │   数据库      │  │\n└───────────────────────┘       │  └──────────────┘  │\n                                └────────────────────┘\n",[45,9989,9987],{"__ignoreMap":81},[113,9991,9993],{"id":9992},"️-前端sublog-react","🖥️ 前端：sublog-react",[15,9995,9996,9997,9999,10000,10003],{},"选用 ",[19,9998,2622],{},"（App Router） + ",[19,10001,10002],{},"React 19","，理由是：",[177,10005,10006,10013,10020,10027],{},[29,10007,10008,10009,10012],{},"🎨 ",[19,10010,10011],{},"React 生态","——组件库成熟，开发效率高",[29,10014,10015,10016,10019],{},"📐 ",[19,10017,10018],{},"App Router","——支持布局嵌套，页面组织清晰",[29,10021,10022,10023,10026],{},"📦 ",[19,10024,10025],{},"静态导出","——仍可部署到静态托管",[29,10028,10029,10030,10033],{},"⚙️ ",[19,10031,10032],{},"运行时配置","——API 地址可动态注入，切换环境无需重新构建",[15,10035,10036],{},"前端不再直接读取 Markdown 文件，而是通过 HTTP API 获取文章数据。静态导出 + 客户端数据获取的组合，既保留了首屏加载速度，又实现了内容的动态更新。",[113,10038,10040],{"id":10039},"️-后端sublog-s","⚙️ 后端：sublog-s",[15,10042,10043,10044,2456,10046,10048],{},"后端使用 ",[19,10045,2076],{},[19,10047,6642],{},"，按业务逻辑分层组织代码。",[15,10050,10051],{},"核心技术选型：",[249,10053,10054,10062],{},[252,10055,10056],{},[255,10057,10058,10060],{},[258,10059,2753],{},[258,10061,2756],{},[265,10063,10064,10074,10085],{},[255,10065,10066,10071],{},[270,10067,10068,10069],{},"🚀 ",[19,10070,6642],{},[270,10072,10073],{},"HTTP 框架",[255,10075,10076,10082],{},[270,10077,10078,10079],{},"🗄️ ",[19,10080,10081],{},"GORM",[270,10083,10084],{},"ORM，数据库操作",[255,10086,10087,10093],{},[270,10088,10089,10090],{},"🔐 ",[19,10091,10092],{},"JWT",[270,10094,10095],{},"用户认证",[11,10097,10099],{"id":10098},"前后对比","📊 前后对比",[249,10101,10102,10115],{},[252,10103,10104],{},[255,10105,10106,10109,10112],{},[258,10107,10108],{},"维度",[258,10110,10111],{},"第一代（纯静态）",[258,10113,10114],{},"第二代（前后端分离）",[265,10116,10117,10129,10141,10155,10168,10181,10195,10207],{},[255,10118,10119,10123,10126],{},[270,10120,10008,10121],{},[19,10122,2718],{},[270,10124,10125],{},"Nuxt 3 (Vue)",[270,10127,10128],{},"Next.js (React 19)",[255,10130,10131,10136,10138],{},[270,10132,10133,10134],{},"🖥️ ",[19,10135,6644],{},[270,10137,2661],{},[270,10139,10140],{},"Go + Gin",[255,10142,10143,10149,10152],{},[270,10144,10145,10146],{},"📄 ",[19,10147,10148],{},"内容来源",[270,10150,10151],{},"Markdown 文件",[270,10153,10154],{},"API 数据库",[255,10156,10157,10162,10165],{},[270,10158,10068,10159],{},[19,10160,10161],{},"发布方式",[270,10163,10164],{},"全量重新构建",[270,10166,10167],{},"API 实时发布",[255,10169,10170,10175,10178],{},[270,10171,9937,10172],{},[19,10173,10174],{},"用户系统",[270,10176,10177],{},"❌ 无",[270,10179,10180],{},"✅ 注册\u002F登录",[255,10182,10183,10189,10192],{},[270,10184,10185,10186],{},"💬 ",[19,10187,10188],{},"评论",[270,10190,10191],{},"Giscus（需 GitHub）",[270,10193,10194],{},"自有评论系统（规划中）",[255,10196,10197,10201,10204],{},[270,10198,10022,10199],{},[19,10200,3029],{},[270,10202,10203],{},"1 个静态目录",[270,10205,10206],{},"前端静态 + 后端二进制",[255,10208,10209,10215,10218],{},[270,10210,10211,10212],{},"⚡ ",[19,10213,10214],{},"更新体验",[270,10216,10217],{},"改文章 → 构建 → 部署",[270,10219,10220],{},"改文章 → 立即生效",[11,10222,10224],{"id":10223},"新功能注册账号","🎉 新功能：注册账号",[15,10226,10227,10228,10231],{},"伴随着架构升级，",[19,10229,10230],{},"用户账号系统","也上线了。",[113,10233,10235],{"id":10234},"目前支持","📋 目前支持",[15,10237,10238,10239,73],{},"现在你可以在尼克的小窝 ",[19,10240,10241],{},"注册一个账号",[75,10243,10246],{"className":10244,"code":10245,"language":80},[78],"1. ✏️ 填写用户名、邮箱、密码\n2. 📧 查收激活邮件\n3. 🔗 点击链接激活账号\n4. 🚪 登录后查看和管理个人信息\n",[45,10247,10245],{"__ignoreMap":81},[11,10249,10251],{"id":10250},"️-后续计划","🗓️ 后续计划",[15,10253,10254],{},"正在开发或规划中的功能：",[177,10256,10258,10264,10270,10276,10282,10288,10294,10300],{"className":10257},[3062],[29,10259,10261,10263],{"className":10260},[3066],[3068,10262],{"checked":666,"disabled":666,"type":3070}," 用户注册\u002F登录（已上线 🎉）",[29,10265,10267,10269],{"className":10266},[3066],[3068,10268],{"checked":666,"disabled":666,"type":3070}," 个人资料编辑（已上线 🎉）",[29,10271,10273,10275],{"className":10272},[3066],[3068,10274],{"disabled":666,"type":3070}," 💬 站内评论系统",[29,10277,10279,10281],{"className":10278},[3066],[3068,10280],{"disabled":666,"type":3070}," 🔖 文章收藏",[29,10283,10285,10287],{"className":10284},[3066],[3068,10286],{"disabled":666,"type":3070}," 📚 阅读历史",[29,10289,10291,10293],{"className":10290},[3066],[3068,10292],{"disabled":666,"type":3070}," 👍 文章点赞",[29,10295,10297,10299],{"className":10296},[3066],[3068,10298],{"disabled":666,"type":3070}," 🔔 关注通知",[29,10301,10303,10305],{"className":10302},[3066],[3068,10304],{"disabled":666,"type":3070}," 🎨 个人首页定制",[11,10307,10309],{"id":10308},"结语","💡 结语",[15,10311,10312,10313,10316],{},"从 Nuxt 3 纯静态到 React + Go 前后端分离，SuBlog 的架构变迁本质上是一个",[19,10314,10315],{},"功能驱动","的过程。静态博客有它的位置——写写文章、展示内容完全够用。但当你想做的不仅仅是\"展示\"时，后端的能力就不可或缺了。",[15,10318,10319,10320,10323],{},"这次重构最大的感触是：",[19,10321,10322],{},"架构选型不是越复杂越好，而是刚好够用最好","。纯静态阶段用 Nuxt 3 一点问题没有，直到功能需求超出了静态的边界，才自然推动到前后端分离。",[15,10325,10326,10327],{},"如果你也在纠结\"要不要上后端\"，不妨先问问自己：",[19,10328,10329],{},"你要做的是\"展示内容\"还是\"提供服务\"？",[15,10331,10332],{},"前者，静态就够了；后者，来吧，后端欢迎你。😄",[3105,10334],{},[15,10336,10337],{},[19,10338,10339],{},"🎉 目前注册功能已经可以用了，后续更多好玩的功能正在路上。部分功能可能需要登录才能体验，快注册一个账号吧！",{"title":81,"searchDepth":223,"depth":244,"links":10341},[10342,10343,10349,10354,10355,10358,10359],{"id":13,"depth":223,"text":9821},{"id":9838,"depth":223,"text":9839,"children":10344},[10345,10346,10347,10348],{"id":9842,"depth":244,"text":9843},{"id":9852,"depth":244,"text":9853},{"id":260,"depth":244,"text":9887},{"id":9904,"depth":244,"text":9905},{"id":9979,"depth":223,"text":9980,"children":10350},[10351,10352,10353],{"id":9983,"depth":244,"text":9843},{"id":9992,"depth":244,"text":9993},{"id":10039,"depth":244,"text":10040},{"id":10098,"depth":223,"text":10099},{"id":10223,"depth":223,"text":10224,"children":10356},[10357],{"id":10234,"depth":244,"text":10235},{"id":10250,"depth":223,"text":10251},{"id":10308,"depth":223,"text":10309},"记录尼克的小窝从 Nuxt 3 纯静态博客演进到 React + Go 前后端分离架构的全过程，包括架构设计的思考、技术选型的变化，以及新增的账号注册功能。","\u002Fposts\u002Fsublog-evolution\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsublog-evolution","2026-07-23T12:00:00",{"title":9816,"description":10360},"尼克的小窝 开发记录","posts\u002Fsublog-evolution\u002Findex",[10369,10370,2622,2076],"架构","React","2026-07-23","VMQ6A2K8GQ5t3DSlezWPd3KgXnmDVMn9QVeaI28Hcp8",{"id":4,"title":5,"author":6,"body":10374,"description":1258,"difficulty":1259,"draft":1260,"extension":1261,"image":1262,"meta":11284,"navigation":666,"path":1264,"pinned":1260,"published":1265,"seo":11285,"series":6,"seriesOrder":6,"stem":1267,"tags":11286,"updated":1272,"__hash__":1273},{"type":8,"value":10375,"toc":11260},[10376,10378,10382,10384,10392,10398,10400,10404,10412,10417,10423,10427,10429,10433,10435,10441,10443,10449,10451,10453,10457,10462,10464,10466,10470,10482,10484,10486,10488,10512,10544,10548,10550,10662,10692,10698,10700,10732,10736,10738,10744,10746,10768,10770,10772,10774,10776,10944,10946,11018,11020,11024,11118,11120,11126,11204,11206,11208,11232,11234,11236,11242,11256,11258],[11,10377,13],{"id":13},[15,10379,17,10380],{},[19,10381,21],{},[15,10383,24],{},[26,10385,10386,10388,10390],{},[29,10387,31],{},[29,10389,34],{},[29,10391,37],{},[39,10393,10394],{},[15,10395,43,10396,48],{},[45,10397,47],{},[11,10399,52],{"id":51},[15,10401,55,10402,59],{},[19,10403,58],{},[15,10405,62,10406,73],{},[19,10407,65,10408,72],{},[19,10409,68,10410,71],{},[45,10411,47],{},[75,10413,10415],{"className":10414,"code":79,"language":80,"meta":81},[78],[45,10416,79],{"__ignoreMap":81},[15,10418,86,10419,90,10421,94],{},[19,10420,89],{},[45,10422,93],{},[15,10424,97,10425,101],{},[19,10426,100],{},[11,10428,104],{"id":104},[15,10430,107,10431,111],{},[19,10432,110],{},[113,10434,116],{"id":115},[15,10436,119,10437,123,10439,127],{},[45,10438,122],{},[45,10440,126],{},[113,10442,131],{"id":130},[15,10444,134,10445,138,10447,142],{},[19,10446,137],{},[45,10448,141],{},[15,10450,145],{},[113,10452,149],{"id":148},[15,10454,152,10455,155],{},[45,10456,93],{},[75,10458,10460],{"className":10459,"code":159,"language":80,"meta":81},[78],[45,10461,159],{"__ignoreMap":81},[15,10463,164],{},[113,10465,168],{"id":167},[15,10467,171,10468,175],{},[19,10469,174],{},[177,10471,10472,10474,10480],{},[29,10473,181],{},[29,10475,184,10476,191],{},[19,10477,187,10478,190],{},[45,10479,93],{},[29,10481,194],{},[11,10483,197],{"id":197},[15,10485,200],{},[113,10487,204],{"id":203},[75,10489,10490],{"className":207,"code":208,"language":209,"meta":81,"style":81},[45,10491,10492,10496,10508],{"__ignoreMap":81},[213,10493,10494],{"class":215,"line":216},[213,10495,220],{"class":219},[213,10497,10498,10500,10502,10504,10506],{"class":215,"line":223},[213,10499,227],{"class":226},[213,10501,231],{"class":230},[213,10503,234],{"class":226},[213,10505,238],{"class":237},[213,10507,241],{"class":226},[213,10509,10510],{"class":215,"line":244},[213,10511,247],{"class":219},[249,10513,10514,10522],{},[252,10515,10516],{},[255,10517,10518,10520],{},[258,10519,260],{},[258,10521,263],{},[265,10523,10524,10530,10536],{},[255,10525,10526,10528],{},[270,10527,272],{},[270,10529,275],{},[255,10531,10532,10534],{},[270,10533,280],{},[270,10535,283],{},[255,10537,10538,10540],{},[270,10539,288],{},[270,10541,291,10542],{},[19,10543,294],{},[15,10545,10546,300],{},[19,10547,299],{},[113,10549,304],{"id":303},[75,10551,10552],{"className":207,"code":307,"language":209,"meta":81,"style":81},[45,10553,10554,10558,10580,10588,10612,10624,10636,10644,10648,10658],{"__ignoreMap":81},[213,10555,10556],{"class":215,"line":216},[213,10557,314],{"class":219},[213,10559,10560,10562,10564,10566,10568,10570,10572,10574,10576,10578],{"class":215,"line":223},[213,10561,319],{"class":226},[213,10563,231],{"class":230},[213,10565,324],{"class":226},[213,10567,327],{"class":237},[213,10569,330],{"class":226},[213,10571,333],{"class":237},[213,10573,336],{"class":226},[213,10575,340],{"class":339},[213,10577,343],{"class":230},[213,10579,346],{"class":226},[213,10581,10582,10584,10586],{"class":215,"line":244},[213,10583,351],{"class":230},[213,10585,354],{"class":237},[213,10587,357],{"class":226},[213,10589,10590,10592,10594,10596,10598,10600,10602,10604,10606,10608,10610],{"class":215,"line":360},[213,10591,363],{"class":226},[213,10593,231],{"class":230},[213,10595,368],{"class":226},[213,10597,371],{"class":237},[213,10599,374],{"class":226},[213,10601,378],{"class":377},[213,10603,381],{"class":226},[213,10605,384],{"class":377},[213,10607,381],{"class":226},[213,10609,389],{"class":339},[213,10611,392],{"class":226},[213,10613,10614,10616,10618,10620,10622],{"class":215,"line":395},[213,10615,398],{"class":226},[213,10617,231],{"class":230},[213,10619,403],{"class":226},[213,10621,406],{"class":237},[213,10623,409],{"class":226},[213,10625,10626,10628,10630,10632,10634],{"class":215,"line":412},[213,10627,415],{"class":230},[213,10629,418],{"class":226},[213,10631,421],{"class":230},[213,10633,424],{"class":339},[213,10635,427],{"class":226},[213,10637,10638,10640,10642],{"class":215,"line":430},[213,10639,433],{"class":230},[213,10641,436],{"class":377},[213,10643,439],{"class":219},[213,10645,10646],{"class":215,"line":442},[213,10647,445],{"class":226},[213,10649,10650,10652,10654,10656],{"class":215,"line":448},[213,10651,351],{"class":230},[213,10653,453],{"class":226},[213,10655,456],{"class":237},[213,10657,357],{"class":226},[213,10659,10660],{"class":215,"line":461},[213,10661,464],{"class":219},[249,10663,10664,10672],{},[252,10665,10666],{},[255,10667,10668,10670],{},[258,10669,260],{},[258,10671,263],{},[265,10673,10674,10680,10686],{},[255,10675,10676,10678],{},[270,10677,481],{},[270,10679,484],{},[255,10681,10682,10684],{},[270,10683,489],{},[270,10685,492],{},[255,10687,10688,10690],{},[270,10689,497],{},[270,10691,500],{},[15,10693,10694,505,10696,509],{},[19,10695,299],{},[19,10697,508],{},[113,10699,513],{"id":512},[249,10701,10702,10710],{},[252,10703,10704],{},[255,10705,10706,10708],{},[258,10707,260],{},[258,10709,263],{},[265,10711,10712,10718,10726],{},[255,10713,10714,10716],{},[270,10715,530],{},[270,10717,533],{},[255,10719,10720,10724],{},[270,10721,538,10722,542],{},[45,10723,541],{},[270,10725,545],{},[255,10727,10728,10730],{},[270,10729,550],{},[270,10731,553],{},[15,10733,10734,558],{},[19,10735,299],{},[11,10737,562],{"id":561},[15,10739,565,10740,569,10742,573],{},[19,10741,568],{},[19,10743,572],{},[15,10745,576],{},[26,10747,10748,10754,10758,10764],{},[29,10749,10750,584,10752,588],{},[19,10751,583],{},[45,10753,587],{},[29,10755,10756,594],{},[19,10757,593],{},[29,10759,10760,600,10762,604],{},[19,10761,599],{},[45,10763,603],{},[29,10765,10766,610],{},[19,10767,609],{},[15,10769,613],{},[11,10771,617],{"id":616},[15,10773,620],{},[113,10775,624],{"id":623},[75,10777,10778],{"className":207,"code":627,"language":209,"meta":81,"style":81},[45,10779,10780,10786,10794,10802,10806,10810,10814,10836,10840,10852,10870,10882,10888,10892,10904,10916,10922,10926,10934,10940],{"__ignoreMap":81},[213,10781,10782,10784],{"class":215,"line":216},[213,10783,634],{"class":230},[213,10785,637],{"class":226},[213,10787,10788,10790,10792],{"class":215,"line":223},[213,10789,642],{"class":377},[213,10791,645],{"class":237},[213,10793,648],{"class":377},[213,10795,10796,10798,10800],{"class":215,"line":244},[213,10797,642],{"class":377},[213,10799,655],{"class":237},[213,10801,648],{"class":377},[213,10803,10804],{"class":215,"line":360},[213,10805,392],{"class":226},[213,10807,10808],{"class":215,"line":395},[213,10809,667],{"emptyLinePlaceholder":666},[213,10811,10812],{"class":215,"line":412},[213,10813,672],{"class":219},[213,10815,10816,10818,10820,10822,10824,10826,10828,10830,10832,10834],{"class":215,"line":430},[213,10817,677],{"class":230},[213,10819,680],{"class":226},[213,10821,683],{"class":237},[213,10823,686],{"class":226},[213,10825,689],{"class":237},[213,10827,692],{"class":226},[213,10829,695],{"class":237},[213,10831,686],{"class":226},[213,10833,700],{"class":237},[213,10835,703],{"class":226},[213,10837,10838],{"class":215,"line":442},[213,10839,667],{"emptyLinePlaceholder":666},[213,10841,10842,10844,10846,10848,10850],{"class":215,"line":448},[213,10843,712],{"class":230},[213,10845,715],{"class":237},[213,10847,718],{"class":226},[213,10849,721],{"class":230},[213,10851,427],{"class":226},[213,10853,10854,10856,10858,10860,10862,10864,10866,10868],{"class":215,"line":461},[213,10855,728],{"class":226},[213,10857,231],{"class":230},[213,10859,733],{"class":226},[213,10861,736],{"class":237},[213,10863,739],{"class":226},[213,10865,742],{"class":377},[213,10867,745],{"class":226},[213,10869,748],{"class":219},[213,10871,10872,10874,10876,10878,10880],{"class":215,"line":751},[213,10873,754],{"class":230},[213,10875,418],{"class":226},[213,10877,421],{"class":230},[213,10879,424],{"class":339},[213,10881,427],{"class":226},[213,10883,10884,10886],{"class":215,"line":765},[213,10885,768],{"class":230},[213,10887,771],{"class":226},[213,10889,10890],{"class":215,"line":774},[213,10891,777],{"class":226},[213,10893,10894,10896,10898,10900,10902],{"class":215,"line":780},[213,10895,783],{"class":226},[213,10897,231],{"class":230},[213,10899,788],{"class":226},[213,10901,791],{"class":237},[213,10903,794],{"class":226},[213,10905,10906,10908,10910,10912,10914],{"class":215,"line":797},[213,10907,754],{"class":230},[213,10909,418],{"class":226},[213,10911,421],{"class":230},[213,10913,424],{"class":339},[213,10915,427],{"class":226},[213,10917,10918,10920],{"class":215,"line":810},[213,10919,768],{"class":230},[213,10921,771],{"class":226},[213,10923,10924],{"class":215,"line":817},[213,10925,777],{"class":226},[213,10927,10928,10930,10932],{"class":215,"line":822},[213,10929,825],{"class":226},[213,10931,828],{"class":237},[213,10933,831],{"class":226},[213,10935,10936,10938],{"class":215,"line":834},[213,10937,433],{"class":230},[213,10939,839],{"class":339},[213,10941,10942],{"class":215,"line":842},[213,10943,445],{"class":226},[113,10945,848],{"id":847},[75,10947,10948],{"className":207,"code":851,"language":209,"meta":81,"style":81},[45,10949,10950,10968,10984,10996,11002,11006,11014],{"__ignoreMap":81},[213,10951,10952,10954,10956,10958,10960,10962,10964,10966],{"class":215,"line":216},[213,10953,712],{"class":230},[213,10955,860],{"class":237},[213,10957,739],{"class":226},[213,10959,866],{"class":865},[213,10961,869],{"class":230},[213,10963,745],{"class":226},[213,10965,874],{"class":230},[213,10967,427],{"class":226},[213,10969,10970,10972,10974,10976,10978,10980,10982],{"class":215,"line":223},[213,10971,881],{"class":226},[213,10973,231],{"class":230},[213,10975,234],{"class":226},[213,10977,888],{"class":237},[213,10979,891],{"class":226},[213,10981,238],{"class":237},[213,10983,241],{"class":226},[213,10985,10986,10988,10990,10992,10994],{"class":215,"line":244},[213,10987,754],{"class":230},[213,10989,418],{"class":226},[213,10991,421],{"class":230},[213,10993,424],{"class":339},[213,10995,427],{"class":226},[213,10997,10998,11000],{"class":215,"line":360},[213,10999,768],{"class":230},[213,11001,914],{"class":377},[213,11003,11004],{"class":215,"line":395},[213,11005,777],{"class":226},[213,11007,11008,11010,11012],{"class":215,"line":412},[213,11009,433],{"class":230},[213,11011,925],{"class":226},[213,11013,928],{"class":219},[213,11015,11016],{"class":215,"line":430},[213,11017,445],{"class":226},[113,11019,936],{"id":935},[15,11021,939,11022,943],{},[45,11023,942],{},[75,11025,11026],{"className":207,"code":946,"language":209,"meta":81,"style":81},[45,11027,11028,11040,11044,11062,11076,11086,11090,11100,11108,11114],{"__ignoreMap":81},[213,11029,11030,11032,11034,11036,11038],{"class":215,"line":216},[213,11031,677],{"class":230},[213,11033,955],{"class":226},[213,11035,958],{"class":237},[213,11037,686],{"class":226},[213,11039,963],{"class":237},[213,11041,11042],{"class":215,"line":223},[213,11043,667],{"emptyLinePlaceholder":666},[213,11045,11046,11048,11050,11052,11054,11056,11058,11060],{"class":215,"line":244},[213,11047,712],{"class":230},[213,11049,974],{"class":237},[213,11051,739],{"class":226},[213,11053,866],{"class":865},[213,11055,869],{"class":230},[213,11057,745],{"class":226},[213,11059,874],{"class":230},[213,11061,427],{"class":226},[213,11063,11064,11066,11068,11070,11072,11074],{"class":215,"line":360},[213,11065,754],{"class":230},[213,11067,993],{"class":226},[213,11069,231],{"class":230},[213,11071,998],{"class":226},[213,11073,888],{"class":237},[213,11075,1003],{"class":226},[213,11077,11078,11080,11082,11084],{"class":215,"line":395},[213,11079,768],{"class":230},[213,11081,1010],{"class":226},[213,11083,874],{"class":230},[213,11085,392],{"class":226},[213,11087,11088],{"class":215,"line":412},[213,11089,777],{"class":226},[213,11091,11092,11094,11096,11098],{"class":215,"line":430},[213,11093,1023],{"class":226},[213,11095,231],{"class":230},[213,11097,860],{"class":237},[213,11099,241],{"class":226},[213,11101,11102,11104,11106],{"class":215,"line":442},[213,11103,1034],{"class":226},[213,11105,828],{"class":237},[213,11107,1039],{"class":226},[213,11109,11110,11112],{"class":215,"line":448},[213,11111,433],{"class":230},[213,11113,1046],{"class":226},[213,11115,11116],{"class":215,"line":461},[213,11117,445],{"class":226},[113,11119,1054],{"id":1053},[15,11121,1057,11122,1060,11124,1063],{},[45,11123,791],{},[45,11125,828],{},[75,11127,11128],{"className":207,"code":1066,"language":209,"meta":81,"style":81},[45,11129,11130,11150,11162,11174,11180,11184,11194,11200],{"__ignoreMap":81},[213,11131,11132,11134,11136,11138,11140,11142,11144,11146,11148],{"class":215,"line":216},[213,11133,712],{"class":230},[213,11135,1075],{"class":237},[213,11137,739],{"class":226},[213,11139,1080],{"class":865},[213,11141,1083],{"class":226},[213,11143,1086],{"class":230},[213,11145,745],{"class":226},[213,11147,721],{"class":230},[213,11149,427],{"class":226},[213,11151,11152,11154,11156,11158,11160],{"class":215,"line":223},[213,11153,783],{"class":226},[213,11155,231],{"class":230},[213,11157,788],{"class":226},[213,11159,791],{"class":237},[213,11161,1105],{"class":226},[213,11163,11164,11166,11168,11170,11172],{"class":215,"line":244},[213,11165,754],{"class":230},[213,11167,418],{"class":226},[213,11169,421],{"class":230},[213,11171,424],{"class":339},[213,11173,427],{"class":226},[213,11175,11176,11178],{"class":215,"line":360},[213,11177,768],{"class":230},[213,11179,771],{"class":226},[213,11181,11182],{"class":215,"line":395},[213,11183,777],{"class":226},[213,11185,11186,11188,11190,11192],{"class":215,"line":412},[213,11187,825],{"class":226},[213,11189,828],{"class":237},[213,11191,1136],{"class":226},[213,11193,1139],{"class":219},[213,11195,11196,11198],{"class":215,"line":430},[213,11197,433],{"class":230},[213,11199,839],{"class":339},[213,11201,11202],{"class":215,"line":442},[213,11203,445],{"class":226},[11,11205,1152],{"id":1152},[15,11207,1155],{},[177,11209,11210,11214,11218,11222,11226],{},[29,11211,11212,1163],{},[19,11213,1162],{},[29,11215,11216,1169],{},[19,11217,1168],{},[29,11219,11220,1175],{},[19,11221,1174],{},[29,11223,11224,1181],{},[19,11225,1180],{},[29,11227,11228,1187,11230,1191],{},[19,11229,1186],{},[19,11231,1190],{},[11,11233,1194],{"id":1194},[15,11235,1197],{},[39,11237,11238],{},[15,11239,11240,1205],{},[19,11241,1204],{},[177,11243,11244,11248,11252],{},[29,11245,1210,11246,1213],{},[19,11247,110],{},[29,11249,1216,11250,1220],{},[19,11251,1219],{},[29,11253,1223,11254,59],{},[19,11255,1226],{},[15,11257,1229],{},[1231,11259,1233],{},{"title":81,"searchDepth":223,"depth":244,"links":11261},[11262,11263,11264,11270,11275,11276,11282,11283],{"id":13,"depth":223,"text":13},{"id":51,"depth":223,"text":52},{"id":104,"depth":223,"text":104,"children":11265},[11266,11267,11268,11269],{"id":115,"depth":244,"text":116},{"id":130,"depth":244,"text":131},{"id":148,"depth":244,"text":149},{"id":167,"depth":244,"text":168},{"id":197,"depth":223,"text":197,"children":11271},[11272,11273,11274],{"id":203,"depth":244,"text":204},{"id":303,"depth":244,"text":304},{"id":512,"depth":244,"text":513},{"id":561,"depth":223,"text":562},{"id":616,"depth":223,"text":617,"children":11277},[11278,11279,11280,11281],{"id":623,"depth":244,"text":624},{"id":847,"depth":244,"text":848},{"id":935,"depth":244,"text":936},{"id":1053,"depth":244,"text":1054},{"id":1152,"depth":223,"text":1152},{"id":1194,"depth":223,"text":1194},{},{"title":5,"description":1258},[1269,1270,1271],{"id":11288,"title":11289,"author":6,"body":11290,"description":12812,"difficulty":2066,"draft":1260,"extension":1261,"image":12813,"meta":12814,"navigation":666,"path":12815,"pinned":1260,"published":12816,"seo":12817,"series":6638,"seriesOrder":244,"stem":12818,"tags":12819,"updated":1272,"__hash__":12822},"posts\u002Fposts\u002Fgo-embed-react-single-binary\u002Findex.md","Go 单文件打包进阶：React 前端 + 交叉编译，一次踩完所有坑",{"type":8,"value":11291,"toc":12791},[11292,11294,11305,11316,11323,11326,11362,11364,11373,11375,11379,11389,11433,11475,11493,11497,11518,11549,11553,11845,11848,11886,11890,12233,12236,12240,12437,12447,12449,12460,12474,12487,12498,12502,12509,12525,12538,12543,12547,12563,12568,12613,12627,12638,12642,12657,12666,12688,12697,12701,12711,12713,12740,12749,12751,12754,12781,12788],[11,11293,3144],{"id":3144},[15,11295,11296,11297,11300,11301,11304],{},"这是「Go 单文件打包」系列第三篇。上一篇（",[3097,11298,11299],{"href":8395},"把前端塞进二进制","）记录了一个 Gin + Vue 3 项目的 embed 方案。这次换了个项目继续踩坑：",[19,11302,11303],{},"Magic Img","——一个自托管、本地优先的 AI 生图提示词管理工具，技术栈是：",[177,11306,11307,11310],{},[29,11308,11309],{},"前端：React 19 + TypeScript + Vite + Tailwind v4（UI 库 Magic UI + shadcn\u002Fui）",[29,11311,11312,11313,7182],{},"后端：Go + Gin + GORM + SQLite（",[19,11314,11315],{},"modernc 纯 Go 驱动，无 cgo",[15,11317,11318,11319,11322],{},"目标一样：",[19,11320,11321],{},"一个二进制拖到任何机器上就能跑","——前端、API、数据库全部内嵌。",[15,11324,11325],{},"和上篇不同的地方有三个，正好是这篇要讲的重点：",[26,11327,11328,11342,11355],{},[29,11329,11330,11331,11337,11338,11341],{},"前端用了 ",[19,11332,11333,11334],{},"HashRouter + Vite base ",[45,11335,11336],{},"\u002Fdist","，服务端",[19,11339,11340],{},"不再需要 SPA fallback","（上篇坑一的新解法）；",[29,11343,11344,11345,11351,11352,11354],{},"embed 用 ",[19,11346,11347,11350],{},[45,11348,11349],{},"all:"," 前缀","一劳永逸解决 ",[45,11353,8089],{}," 开头文件被排除的问题（上篇坑二的升级解法）；",[29,11356,11357,11358,11361],{},"需要",[19,11359,11360],{},"在 Windows 上交叉编译 Linux 产物","，踩了「生成的文件无法在 Linux 执行」的坑。",[11,11363,7448],{"id":7448},[15,11365,11366,11367,11369,11370,11372],{},"和上篇一致：",[45,11368,7454],{}," 在编译期把前端 ",[45,11371,7458],{}," 塞进二进制，运行时由 Go 直接提供静态服务。差异在于路由策略和缓存策略，后面细说。",[11,11374,7462],{"id":7462},[113,11376,11378],{"id":11377},"_1-前端hash-路由-base-dist","1. 前端：hash 路由 + base \u002Fdist",[15,11380,11381,11382,11385,11386,73],{},"vite.config.ts 里固定 ",[45,11383,11384],{},"base: \"\u002Fdist\u002F\"","，路由用 ",[45,11387,11388],{},"HashRouter",[75,11390,11392],{"className":8140,"code":11391,"language":8142,"meta":81,"style":81},"\u002F\u002F vite.config.ts\nexport default defineConfig({\n  base: \"\u002Fdist\u002F\",   \u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n  ...\n})\n",[45,11393,11394,11398,11410,11424,11429],{"__ignoreMap":81},[213,11395,11396],{"class":215,"line":216},[213,11397,8149],{"class":219},[213,11399,11400,11402,11404,11407],{"class":215,"line":223},[213,11401,1380],{"class":230},[213,11403,4008],{"class":230},[213,11405,11406],{"class":237}," defineConfig",[213,11408,11409],{"class":226},"({\n",[213,11411,11412,11415,11418,11421],{"class":215,"line":244},[213,11413,11414],{"class":226},"  base: ",[213,11416,11417],{"class":377},"\"\u002Fdist\u002F\"",[213,11419,11420],{"class":226},",   ",[213,11422,11423],{"class":219},"\u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n",[213,11425,11426],{"class":215,"line":360},[213,11427,11428],{"class":230},"  ...\n",[213,11430,11431],{"class":215,"line":395},[213,11432,3815],{"class":226},[75,11434,11438],{"className":11435,"code":11436,"language":11437,"meta":81,"style":81},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n\u003CHashRouter>\n  \u003CRoutes>...\u003C\u002FRoutes>\n\u003C\u002FHashRouter>\n","tsx",[45,11439,11440,11445,11453,11467],{"__ignoreMap":81},[213,11441,11442],{"class":215,"line":216},[213,11443,11444],{"class":219},"\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n",[213,11446,11447,11449,11451],{"class":215,"line":223},[213,11448,2206],{"class":226},[213,11450,11388],{"class":339},[213,11452,1864],{"class":226},[213,11454,11455,11457,11460,11463,11465],{"class":215,"line":244},[213,11456,3842],{"class":226},[213,11458,11459],{"class":339},"Routes",[213,11461,11462],{"class":226},">...\u003C\u002F",[213,11464,11459],{"class":339},[213,11466,1864],{"class":226},[213,11468,11469,11471,11473],{"class":215,"line":360},[213,11470,3820],{"class":226},[213,11472,11388],{"class":339},[213,11474,1864],{"class":226},[15,11476,11477,11480,11481,11484,11485,11488,11489,11492],{},[19,11478,11479],{},"为什么这样选","：history 路由需要服务端把未知路径全部 fallback 回 ",[45,11482,11483],{},"index.html","（上篇坑一）。hash 路由下浏览器请求的永远是 ",[45,11486,11487],{},"\u002Fdist\u002F"," 这一个入口，",[19,11490,11491],{},"静态服务直接返回 index.html 就完事，一个 fallback 都不用写","。内嵌场景里这是最省心的组合。",[113,11494,11496],{"id":11495},"_2-构建产物复制进后端目录","2. 构建产物复制进后端目录",[15,11498,11499,11502,11503,11505,11506,11509,11510,11513,11514,11517],{},[45,11500,11501],{},"npm run build"," 除了编译，还会把 ",[45,11504,7458],{}," 复制到 ",[45,11507,11508],{},"backend\u002Fweb\u002Fdist","（一个小脚本 ",[45,11511,11512],{},"scripts\u002Fcopy-dist.mjs"," 干这个活），供 ",[45,11515,11516],{},"go:embed"," 使用：",[75,11519,11521],{"className":6465,"code":11520,"language":6467,"meta":81,"style":81},"\u002F\u002F frontend\u002Fpackage.json\n\"scripts\": {\n  \"build\": \"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n}\n",[45,11522,11523,11528,11535,11545],{"__ignoreMap":81},[213,11524,11525],{"class":215,"line":216},[213,11526,11527],{"class":219},"\u002F\u002F frontend\u002Fpackage.json\n",[213,11529,11530,11533],{"class":215,"line":223},[213,11531,11532],{"class":377},"\"scripts\"",[213,11534,3195],{"class":226},[213,11536,11537,11540,11542],{"class":215,"line":244},[213,11538,11539],{"class":339},"  \"build\"",[213,11541,2981],{"class":226},[213,11543,11544],{"class":377},"\"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n",[213,11546,11547],{"class":215,"line":360},[213,11548,445],{"class":226},[113,11550,11552],{"id":11551},"_3-后端embed-httpfilesystem-包装","3. 后端：embed + http.FileSystem 包装",[75,11554,11556],{"className":207,"code":11555,"language":209,"meta":81,"style":81},"\u002F\u002F backend\u002Fweb\u002Fembed.go\npackage web\n\nimport (\n    \"embed\"\n    \"net\u002Fhttp\"\n    \"strings\"\n)\n\n\u002F\u002Fgo:embed all:dist\nvar distFS embed.FS\n\n\u002F\u002F Dist 内嵌前端静态文件系统\nvar Dist http.FileSystem = &fileSystem{prefix: \"dist\", fs: &distFS}\n\ntype fileSystem struct {\n    prefix string\n    fs     *embed.FS\n}\n\nfunc (f *fileSystem) Open(name string) (http.File, error) {\n    if name == \"\u002F\" {\n        name = f.prefix\n    } else {\n        name = f.prefix + \"\u002F\" + strings.TrimPrefix(name, \"\u002F\")\n    }\n    return http.FS(f.fs).Open(name)\n}\n",[45,11557,11558,11563,11569,11573,11579,11587,11595,11603,11607,11611,11616,11629,11633,11638,11675,11679,11691,11699,11712,11716,11720,11759,11773,11783,11793,11820,11824,11841],{"__ignoreMap":81},[213,11559,11560],{"class":215,"line":216},[213,11561,11562],{"class":219},"\u002F\u002F backend\u002Fweb\u002Fembed.go\n",[213,11564,11565,11567],{"class":215,"line":223},[213,11566,7481],{"class":230},[213,11568,7484],{"class":237},[213,11570,11571],{"class":215,"line":244},[213,11572,667],{"emptyLinePlaceholder":666},[213,11574,11575,11577],{"class":215,"line":360},[213,11576,634],{"class":230},[213,11578,637],{"class":226},[213,11580,11581,11583,11585],{"class":215,"line":395},[213,11582,642],{"class":377},[213,11584,7501],{"class":237},[213,11586,648],{"class":377},[213,11588,11589,11591,11593],{"class":215,"line":412},[213,11590,642],{"class":377},[213,11592,7528],{"class":237},[213,11594,648],{"class":377},[213,11596,11597,11599,11601],{"class":215,"line":430},[213,11598,642],{"class":377},[213,11600,7546],{"class":237},[213,11602,648],{"class":377},[213,11604,11605],{"class":215,"line":442},[213,11606,392],{"class":226},[213,11608,11609],{"class":215,"line":448},[213,11610,667],{"emptyLinePlaceholder":666},[213,11612,11613],{"class":215,"line":461},[213,11614,11615],{"class":219},"\u002F\u002Fgo:embed all:dist\n",[213,11617,11618,11620,11623,11625,11627],{"class":215,"line":751},[213,11619,677],{"class":230},[213,11621,11622],{"class":226}," distFS ",[213,11624,7501],{"class":237},[213,11626,686],{"class":226},[213,11628,7588],{"class":237},[213,11630,11631],{"class":215,"line":765},[213,11632,667],{"emptyLinePlaceholder":666},[213,11634,11635],{"class":215,"line":774},[213,11636,11637],{"class":219},"\u002F\u002F Dist 内嵌前端静态文件系统\n",[213,11639,11640,11642,11645,11648,11650,11653,11655,11658,11661,11664,11666,11669,11672],{"class":215,"line":780},[213,11641,677],{"class":230},[213,11643,11644],{"class":226}," Dist ",[213,11646,11647],{"class":237},"http",[213,11649,686],{"class":226},[213,11651,11652],{"class":237},"FileSystem",[213,11654,2915],{"class":230},[213,11656,11657],{"class":230}," &",[213,11659,11660],{"class":237},"fileSystem",[213,11662,11663],{"class":226},"{prefix: ",[213,11665,7628],{"class":377},[213,11667,11668],{"class":226},", fs: ",[213,11670,11671],{"class":230},"&",[213,11673,11674],{"class":226},"distFS}\n",[213,11676,11677],{"class":215,"line":797},[213,11678,667],{"emptyLinePlaceholder":666},[213,11680,11681,11683,11686,11689],{"class":215,"line":810},[213,11682,6069],{"class":230},[213,11684,11685],{"class":237}," fileSystem",[213,11687,11688],{"class":230}," struct",[213,11690,427],{"class":226},[213,11692,11693,11696],{"class":215,"line":817},[213,11694,11695],{"class":226},"    prefix ",[213,11697,11698],{"class":230},"string\n",[213,11700,11701,11704,11706,11708,11710],{"class":215,"line":822},[213,11702,11703],{"class":226},"    fs     ",[213,11705,343],{"class":230},[213,11707,7501],{"class":237},[213,11709,686],{"class":226},[213,11711,7588],{"class":237},[213,11713,11714],{"class":215,"line":834},[213,11715,445],{"class":226},[213,11717,11718],{"class":215,"line":842},[213,11719,667],{"emptyLinePlaceholder":666},[213,11721,11722,11724,11726,11729,11731,11733,11735,11738,11740,11742,11744,11746,11748,11750,11753,11755,11757],{"class":215,"line":1714},[213,11723,712],{"class":230},[213,11725,3314],{"class":226},[213,11727,11728],{"class":865},"f ",[213,11730,343],{"class":230},[213,11732,11660],{"class":237},[213,11734,745],{"class":226},[213,11736,11737],{"class":237},"Open",[213,11739,739],{"class":226},[213,11741,8193],{"class":865},[213,11743,869],{"class":230},[213,11745,4913],{"class":226},[213,11747,11647],{"class":237},[213,11749,686],{"class":226},[213,11751,11752],{"class":237},"File",[213,11754,381],{"class":226},[213,11756,721],{"class":230},[213,11758,4515],{"class":226},[213,11760,11761,11763,11766,11768,11771],{"class":215,"line":1719},[213,11762,754],{"class":230},[213,11764,11765],{"class":226}," name ",[213,11767,5777],{"class":230},[213,11769,11770],{"class":377}," \"\u002F\"",[213,11772,427],{"class":226},[213,11774,11775,11778,11780],{"class":215,"line":2350},[213,11776,11777],{"class":226},"        name ",[213,11779,1386],{"class":230},[213,11781,11782],{"class":226}," f.prefix\n",[213,11784,11785,11788,11791],{"class":215,"line":2364},[213,11786,11787],{"class":226},"    } ",[213,11789,11790],{"class":230},"else",[213,11792,427],{"class":226},[213,11794,11795,11797,11799,11802,11804,11806,11809,11811,11813,11816,11818],{"class":215,"line":2371},[213,11796,11777],{"class":226},[213,11798,1386],{"class":230},[213,11800,11801],{"class":226}," f.prefix ",[213,11803,4082],{"class":230},[213,11805,11770],{"class":377},[213,11807,11808],{"class":230}," +",[213,11810,5839],{"class":226},[213,11812,7666],{"class":237},[213,11814,11815],{"class":226},"(name, ",[213,11817,7672],{"class":377},[213,11819,392],{"class":226},[213,11821,11822],{"class":215,"line":2376},[213,11823,777],{"class":226},[213,11825,11826,11828,11830,11833,11836,11838],{"class":215,"line":2382},[213,11827,433],{"class":230},[213,11829,368],{"class":226},[213,11831,11832],{"class":237},"FS",[213,11834,11835],{"class":226},"(f.fs).",[213,11837,11737],{"class":237},[213,11839,11840],{"class":226},"(name)\n",[213,11842,11843],{"class":215,"line":2390},[213,11844,445],{"class":226},[15,11846,11847],{},"两个细节：",[177,11849,11850,11873],{},[29,11851,11852,11860,11861,8106,11863,11865,11866,11868,11869,11872],{},[19,11853,11854,11857,11858,11350],{},[45,11855,11856],{},"all:dist"," 里的 ",[45,11859,11349],{},"：embed 目录时默认会排除 ",[45,11862,8089],{},[45,11864,686],{}," 开头的文件（上篇坑二就是被这个坑了，当时靠改 Vite 输出名绕开）。",[45,11867,11349],{}," 前缀表示「全部包含」，前端构建产物里那些 ",[45,11870,11871],{},"_xxx.js"," 助手文件直接安全落地，不用再改前端配置；",[29,11874,11875,11878,11879,11881,11882,11885],{},[45,11876,11877],{},"http.FileSystem"," 包装：加了一层 ",[45,11880,7458],{}," 前缀映射，",[45,11883,11884],{},"http.FileServer"," 和 Gin 都能直接用。",[113,11887,11889],{"id":11888},"_4-路由静态服务-缓存策略","4. 路由：静态服务 + 缓存策略",[75,11891,11893],{"className":207,"code":11892,"language":209,"meta":81,"style":81},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\nfunc NewRouter(db *gorm.DB, uploadDir string) *gin.Engine {\n    r := gin.Default()\n    ...\n    dist := http.FileServer(web.Dist)\n    r.GET(\"\u002Fdist\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    r.GET(\"\u002Fdist\u002F*filepath\", func(c *gin.Context) {\n        p := c.Param(\"filepath\")\n        if p == \"\u002F\" || p == \"\u002Findex.html\" {\n            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n            c.Header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n        } else if strings.HasPrefix(p, \"\u002Fassets\u002F\") {\n            \u002F\u002F 带内容 hash 的资源可放心长缓存\n            c.Header(\"Cache-Control\", \"public, max-age=31536000, immutable\")\n        }\n        http.StripPrefix(\"\u002Fdist\", dist).ServeHTTP(c.Writer, c.Request)\n    })\n    r.GET(\"\u002F\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    ...\n}\n",[45,11894,11895,11900,11942,11954,11959,11974,12019,12048,12066,12087,12092,12111,12134,12139,12156,12160,12181,12185,12225,12229],{"__ignoreMap":81},[213,11896,11897],{"class":215,"line":216},[213,11898,11899],{"class":219},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\n",[213,11901,11902,11904,11907,11909,11912,11914,11917,11919,11922,11924,11927,11929,11931,11933,11935,11937,11940],{"class":215,"line":223},[213,11903,712],{"class":230},[213,11905,11906],{"class":237}," NewRouter",[213,11908,739],{"class":226},[213,11910,11911],{"class":865},"db",[213,11913,5502],{"class":230},[213,11915,11916],{"class":237},"gorm",[213,11918,686],{"class":226},[213,11920,11921],{"class":237},"DB",[213,11923,381],{"class":226},[213,11925,11926],{"class":865},"uploadDir",[213,11928,869],{"class":230},[213,11930,745],{"class":226},[213,11932,343],{"class":230},[213,11934,5505],{"class":237},[213,11936,686],{"class":226},[213,11938,11939],{"class":237},"Engine",[213,11941,427],{"class":226},[213,11943,11944,11946,11948,11950,11952],{"class":215,"line":244},[213,11945,5318],{"class":226},[213,11947,231],{"class":230},[213,11949,5323],{"class":226},[213,11951,5326],{"class":237},[213,11953,357],{"class":226},[213,11955,11956],{"class":215,"line":360},[213,11957,11958],{"class":230},"    ...\n",[213,11960,11961,11964,11966,11968,11971],{"class":215,"line":395},[213,11962,11963],{"class":226},"    dist ",[213,11965,231],{"class":230},[213,11967,368],{"class":226},[213,11969,11970],{"class":237},"FileServer",[213,11972,11973],{"class":226},"(web.Dist)\n",[213,11975,11976,11978,11980,11982,11985,11987,11989,11991,11993,11995,11997,11999,12001,12004,12007,12009,12012,12014,12016],{"class":215,"line":412},[213,11977,5456],{"class":226},[213,11979,5485],{"class":237},[213,11981,739],{"class":226},[213,11983,11984],{"class":377},"\"\u002Fdist\"",[213,11986,381],{"class":226},[213,11988,712],{"class":230},[213,11990,739],{"class":226},[213,11992,5499],{"class":865},[213,11994,5502],{"class":230},[213,11996,5505],{"class":237},[213,11998,686],{"class":226},[213,12000,4897],{"class":237},[213,12002,12003],{"class":226},") { c.",[213,12005,12006],{"class":237},"Redirect",[213,12008,739],{"class":226},[213,12010,12011],{"class":339},"302",[213,12013,381],{"class":226},[213,12015,11417],{"class":377},[213,12017,12018],{"class":226},") })\n",[213,12020,12021,12023,12025,12027,12030,12032,12034,12036,12038,12040,12042,12044,12046],{"class":215,"line":430},[213,12022,5456],{"class":226},[213,12024,5485],{"class":237},[213,12026,739],{"class":226},[213,12028,12029],{"class":377},"\"\u002Fdist\u002F*filepath\"",[213,12031,381],{"class":226},[213,12033,712],{"class":230},[213,12035,739],{"class":226},[213,12037,5499],{"class":865},[213,12039,5502],{"class":230},[213,12041,5505],{"class":237},[213,12043,686],{"class":226},[213,12045,4897],{"class":237},[213,12047,4515],{"class":226},[213,12049,12050,12052,12054,12056,12059,12061,12064],{"class":215,"line":442},[213,12051,7659],{"class":226},[213,12053,231],{"class":230},[213,12055,5756],{"class":226},[213,12057,12058],{"class":237},"Param",[213,12060,739],{"class":226},[213,12062,12063],{"class":377},"\"filepath\"",[213,12065,392],{"class":226},[213,12067,12068,12070,12072,12074,12076,12078,12080,12082,12085],{"class":215,"line":448},[213,12069,5771],{"class":230},[213,12071,7681],{"class":226},[213,12073,5777],{"class":230},[213,12075,11770],{"class":377},[213,12077,5872],{"class":230},[213,12079,7681],{"class":226},[213,12081,5777],{"class":230},[213,12083,12084],{"class":377}," \"\u002Findex.html\"",[213,12086,427],{"class":226},[213,12088,12089],{"class":215,"line":461},[213,12090,12091],{"class":219},"            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n",[213,12093,12094,12096,12099,12101,12104,12106,12109],{"class":215,"line":751},[213,12095,5787],{"class":226},[213,12097,12098],{"class":237},"Header",[213,12100,739],{"class":226},[213,12102,12103],{"class":377},"\"Cache-Control\"",[213,12105,381],{"class":226},[213,12107,12108],{"class":377},"\"no-cache, no-store, must-revalidate\"",[213,12110,392],{"class":226},[213,12112,12113,12116,12118,12121,12123,12126,12129,12132],{"class":215,"line":765},[213,12114,12115],{"class":226},"        } ",[213,12117,11790],{"class":230},[213,12119,12120],{"class":230}," if",[213,12122,5839],{"class":226},[213,12124,12125],{"class":237},"HasPrefix",[213,12127,12128],{"class":226},"(p, ",[213,12130,12131],{"class":377},"\"\u002Fassets\u002F\"",[213,12133,4515],{"class":226},[213,12135,12136],{"class":215,"line":774},[213,12137,12138],{"class":219},"            \u002F\u002F 带内容 hash 的资源可放心长缓存\n",[213,12140,12141,12143,12145,12147,12149,12151,12154],{"class":215,"line":780},[213,12142,5787],{"class":226},[213,12144,12098],{"class":237},[213,12146,739],{"class":226},[213,12148,12103],{"class":377},[213,12150,381],{"class":226},[213,12152,12153],{"class":377},"\"public, max-age=31536000, immutable\"",[213,12155,392],{"class":226},[213,12157,12158],{"class":215,"line":797},[213,12159,5825],{"class":226},[213,12161,12162,12165,12168,12170,12172,12175,12178],{"class":215,"line":810},[213,12163,12164],{"class":226},"        http.",[213,12166,12167],{"class":237},"StripPrefix",[213,12169,739],{"class":226},[213,12171,11984],{"class":377},[213,12173,12174],{"class":226},", dist).",[213,12176,12177],{"class":237},"ServeHTTP",[213,12179,12180],{"class":226},"(c.Writer, c.Request)\n",[213,12182,12183],{"class":215,"line":817},[213,12184,5551],{"class":226},[213,12186,12187,12189,12191,12193,12195,12197,12199,12201,12203,12205,12207,12209,12211,12213,12215,12217,12219,12221,12223],{"class":215,"line":822},[213,12188,5456],{"class":226},[213,12190,5485],{"class":237},[213,12192,739],{"class":226},[213,12194,7672],{"class":377},[213,12196,381],{"class":226},[213,12198,712],{"class":230},[213,12200,739],{"class":226},[213,12202,5499],{"class":865},[213,12204,5502],{"class":230},[213,12206,5505],{"class":237},[213,12208,686],{"class":226},[213,12210,4897],{"class":237},[213,12212,12003],{"class":226},[213,12214,12006],{"class":237},[213,12216,739],{"class":226},[213,12218,12011],{"class":339},[213,12220,381],{"class":226},[213,12222,11417],{"class":377},[213,12224,12018],{"class":226},[213,12226,12227],{"class":215,"line":834},[213,12228,11958],{"class":230},[213,12230,12231],{"class":215,"line":842},[213,12232,445],{"class":226},[15,12234,12235],{},"缓存策略是内嵌发布的必修课（坑二细说）：入口禁缓存、hash 资源长缓存。",[113,12237,12239],{"id":12238},"_5-打包脚本windows-版-linux-交叉编译版","5. 打包脚本：Windows 版 + Linux 交叉编译版",[75,12241,12243],{"className":1291,"code":12242,"language":1293,"meta":81,"style":81},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\nset -euo pipefail\ncd \"$(dirname \"$0\")\"\n\nBINARY=\"${BINARY:-magic-img}\"\n\necho \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n(cd frontend && npm run build)\n\necho \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n(cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver)\nchmod +x \"backend\u002F$BINARY\"\n\necho \"[3\u002F3] 打包完成: backend\u002F$BINARY\"\n",[45,12244,12245,12250,12255,12265,12283,12287,12308,12312,12320,12339,12343,12350,12407,12422,12426],{"__ignoreMap":81},[213,12246,12247],{"class":215,"line":216},[213,12248,12249],{"class":219},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[213,12251,12252],{"class":215,"line":223},[213,12253,12254],{"class":219},"# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\n",[213,12256,12257,12259,12262],{"class":215,"line":244},[213,12258,7936],{"class":339},[213,12260,12261],{"class":339}," -euo",[213,12263,12264],{"class":377}," pipefail\n",[213,12266,12267,12269,12272,12275,12277,12280],{"class":215,"line":360},[213,12268,8015],{"class":339},[213,12270,12271],{"class":377}," \"$(",[213,12273,12274],{"class":237},"dirname",[213,12276,1320],{"class":377},[213,12278,12279],{"class":339},"$0",[213,12281,12282],{"class":377},"\")\"\n",[213,12284,12285],{"class":215,"line":395},[213,12286,667],{"emptyLinePlaceholder":666},[213,12288,12289,12292,12294,12297,12299,12302,12305],{"class":215,"line":412},[213,12290,12291],{"class":226},"BINARY",[213,12293,1386],{"class":230},[213,12295,12296],{"class":377},"\"${",[213,12298,12291],{"class":226},[213,12300,12301],{"class":230},":-",[213,12303,12304],{"class":226},"magic-img",[213,12306,12307],{"class":377},"}\"\n",[213,12309,12310],{"class":215,"line":430},[213,12311,667],{"emptyLinePlaceholder":666},[213,12313,12314,12317],{"class":215,"line":442},[213,12315,12316],{"class":339},"echo",[213,12318,12319],{"class":377}," \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n",[213,12321,12322,12324,12326,12329,12331,12333,12335,12337],{"class":215,"line":448},[213,12323,739],{"class":226},[213,12325,8015],{"class":339},[213,12327,12328],{"class":377}," frontend",[213,12330,1358],{"class":226},[213,12332,6812],{"class":237},[213,12334,1308],{"class":377},[213,12336,1431],{"class":377},[213,12338,392],{"class":226},[213,12340,12341],{"class":215,"line":461},[213,12342,667],{"emptyLinePlaceholder":666},[213,12344,12345,12347],{"class":215,"line":751},[213,12346,12316],{"class":339},[213,12348,12349],{"class":377}," \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n",[213,12351,12352,12354,12356,12359,12362,12364,12366,12369,12371,12374,12377,12379,12382,12384,12386,12388,12390,12393,12396,12399,12402,12405],{"class":215,"line":765},[213,12353,739],{"class":226},[213,12355,8015],{"class":339},[213,12357,12358],{"class":377}," backend",[213,12360,12361],{"class":226}," && CGO_ENABLED",[213,12363,1386],{"class":230},[213,12365,3359],{"class":377},[213,12367,12368],{"class":226}," GOOS",[213,12370,1386],{"class":230},[213,12372,12373],{"class":377},"linux",[213,12375,12376],{"class":226}," GOARCH",[213,12378,1386],{"class":230},[213,12380,12381],{"class":377},"amd64",[213,12383,1771],{"class":237},[213,12385,1431],{"class":377},[213,12387,1440],{"class":339},[213,12389,1320],{"class":377},[213,12391,12392],{"class":226},"$BINARY",[213,12394,12395],{"class":377},"\"",[213,12397,12398],{"class":339}," -ldflags=",[213,12400,12401],{"class":377},"\"-s -w\"",[213,12403,12404],{"class":377}," .\u002Fcmd\u002Fserver",[213,12406,392],{"class":226},[213,12408,12409,12412,12415,12418,12420],{"class":215,"line":774},[213,12410,12411],{"class":237},"chmod",[213,12413,12414],{"class":377}," +x",[213,12416,12417],{"class":377}," \"backend\u002F",[213,12419,12392],{"class":226},[213,12421,648],{"class":377},[213,12423,12424],{"class":215,"line":780},[213,12425,667],{"emptyLinePlaceholder":666},[213,12427,12428,12430,12433,12435],{"class":215,"line":797},[213,12429,12316],{"class":339},[213,12431,12432],{"class":377}," \"[3\u002F3] 打包完成: backend\u002F",[213,12434,12392],{"class":226},[213,12436,648],{"class":377},[15,12438,12439,12440,12443,12444,59],{},"Windows 版（",[45,12441,12442],{},"build-win.bat","）同理，只是产物叫 ",[45,12445,12446],{},"magic-img.exe",[11,12448,8049],{"id":8049},[113,12450,12452,12453,12456,12457,12459],{"id":12451},"坑一goembed-dist-静默漏掉-_-开头文件升级解法","坑一：",[45,12454,12455],{},"\u002F\u002Fgo:embed dist"," 静默漏掉 ",[45,12458,8089],{}," 开头文件（升级解法）",[15,12461,12462,12463,12470,12471,12473],{},"上篇说过：embed 目录时",[19,12464,12465,12466,8106,12468,8090],{},"硬编码排除 ",[45,12467,8089],{},[45,12469,686],{},"。上篇的解法是改 Vite 输出文件名去掉 ",[45,12472,8089],{}," 前缀；这次直接用：",[75,12475,12477],{"className":207,"code":12476,"language":209,"meta":81,"style":81},"\u002F\u002Fgo:embed all:dist   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[45,12478,12479],{"__ignoreMap":81},[213,12480,12481,12484],{"class":215,"line":216},[213,12482,12483],{"class":219},"\u002F\u002Fgo:embed all:dist",[213,12485,12486],{"class":219},"   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[15,12488,12489],{},[19,12490,12491,12492,12494,12495,12497],{},"结论：能用 ",[45,12493,11349],{}," 就用 ",[45,12496,11349],{},"，从根上消灭这类\"文件莫名 404\"的问题。",[113,12499,12501],{"id":12500},"坑二开发版是新的打包版却是旧的","坑二：开发版是新的、打包版却是旧的",[15,12503,12504,12505,12508],{},"现象：Vite 开发服务一切正常，",[45,12506,12507],{},":8080\u002Fdist\u002F"," 打包版却是旧界面，强刷也不一定好。",[15,12510,12511,12512,12514,12515,12518,12519,12524],{},"排查过程值得记录：先是怀疑 ",[45,12513,8252],{}," 缓存了旧 embed（上篇坑三的思路），重编重启无效；然后直接抓取 8080 返回的 HTML 和 JS，发现",[19,12516,12517],{},"服务端内容已经是新的","——问题出在",[19,12520,12521,12522],{},"浏览器缓存了 ",[45,12523,11483],{},"：它引用旧 hash 的资源，一直不更新。",[15,12526,12527,12529,12530,12533,12534,12537],{},[19,12528,8067],{},"：入口文件响应 ",[45,12531,12532],{},"Cache-Control: no-store","（见上面路由代码），带 hash 的资源给 ",[45,12535,12536],{},"immutable"," 长缓存。升级后浏览器每次拉新入口，资源文件名一变就自动换新，两全其美。",[39,12539,12540],{},[15,12541,12542],{},"教训：排查\"新旧不一致\"先分清是哪一层——服务端产物、浏览器入口缓存、还是资源缓存。抓包看响应内容再动手。",[113,12544,12546],{"id":12545},"坑三windows-上跑的-build-linuxsh产出的文件-linux-却执行不了","坑三：Windows 上跑的 build-linux.sh，产出的文件 Linux 却执行不了",[15,12548,12549,12550,7299,12553,12559,12560,12562],{},"最初脚本只写了 ",[45,12551,12552],{},"CGO_ENABLED=0 go build",[19,12554,12555,12556],{},"没指定 ",[45,12557,12558],{},"GOOS\u002FGOARCH","。在 Windows 上跑，产出的其实是个 Windows PE 文件（只是没叫 ",[45,12561,8554],{},"），传到 Linux 自然「无法执行」。",[15,12564,12565,12567],{},[19,12566,8067],{},"：脚本里显式交叉编译：",[75,12569,12571],{"className":1291,"code":12570,"language":1293,"meta":81,"style":81},"CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver\n",[45,12572,12573],{"__ignoreMap":81},[213,12574,12575,12578,12580,12582,12584,12586,12588,12590,12592,12594,12596,12598,12600,12602,12604,12606,12608,12610],{"class":215,"line":216},[213,12576,12577],{"class":226},"CGO_ENABLED",[213,12579,1386],{"class":230},[213,12581,3359],{"class":377},[213,12583,12368],{"class":226},[213,12585,1386],{"class":230},[213,12587,12373],{"class":377},[213,12589,12376],{"class":226},[213,12591,1386],{"class":230},[213,12593,12381],{"class":377},[213,12595,1771],{"class":237},[213,12597,1431],{"class":377},[213,12599,1440],{"class":339},[213,12601,1320],{"class":377},[213,12603,12392],{"class":226},[213,12605,12395],{"class":377},[213,12607,12398],{"class":339},[213,12609,12401],{"class":377},[213,12611,12612],{"class":377}," .\u002Fcmd\u002Fserver\n",[15,12614,12615,12616,12619,12620,12623,12624],{},"能这么干的前提是",[19,12617,12618],{},"全依赖树无 cgo","——SQLite 用的是 modernc 纯 Go 驱动，bcrypt 用 ",[45,12621,12622],{},"x\u002Fcrypto","。这也是选型时就要留意的点：",[19,12625,12626],{},"想跨平台交叉编译，先把 cgo 依赖清干净。",[15,12628,12629,12630,12633,12634,12637],{},"另外两个小提醒：Windows 上编出的文件没有 Unix 执行位，上传服务器后记得 ",[45,12631,12632],{},"chmod +x","；",[45,12635,12636],{},"-ldflags=\"-s -w\""," 剥离符号，体积从 30MB+ 降到 26MB。",[113,12639,12641],{"id":12640},"坑四dist-被-gitignore-后embed-直接编译失败","坑四：dist 被 gitignore 后，embed 直接编译失败",[15,12643,12644,12646,12647,12649,12650,12653,12654,12656],{},[45,12645,11508],{}," 是构建产物、入了 .gitignore，但 ",[45,12648,7454],{}," 在",[19,12651,12652],{},"编译期","就要读它——刚 clone 下来直接 ",[45,12655,8252],{}," 会报 embed 找不到文件。",[15,12658,12659,12661,12662,12665],{},[19,12660,8067],{},"：gitignore 里保留 ",[45,12663,12664],{},"dist\u002F.gitkeep","（一个空文件），让目录存在于版本库中：",[75,12667,12671],{"className":12668,"code":12669,"language":12670,"meta":81,"style":81},"language-gitignore shiki shiki-themes github-light github-dark","backend\u002Fweb\u002Fdist\u002F*\n!backend\u002Fweb\u002Fdist\u002F\n!backend\u002Fweb\u002Fdist\u002F.gitkeep\n","gitignore",[45,12672,12673,12678,12683],{"__ignoreMap":81},[213,12674,12675],{"class":215,"line":216},[213,12676,12677],{},"backend\u002Fweb\u002Fdist\u002F*\n",[213,12679,12680],{"class":215,"line":223},[213,12681,12682],{},"!backend\u002Fweb\u002Fdist\u002F\n",[213,12684,12685],{"class":215,"line":244},[213,12686,12687],{},"!backend\u002Fweb\u002Fdist\u002F.gitkeep\n",[15,12689,12690,12691,12693,12694,12696],{},"这样 clone 后可以先 ",[45,12692,8252],{}," 通过，再 ",[45,12695,11501],{}," 灌入真实前端。",[113,12698,12700],{"id":12699},"坑五500kb-chunk-警告别慌","坑五：>500KB chunk 警告，别慌",[15,12702,12703,12704,12707,12708,59],{},"Vite 构建会警告单 chunk 超过 500KB。这个项目里 Base UI 组件库全量引入，单 chunk 约 640KB——",[19,12705,12706],{},"属正常，不要为了消警告去拆配置","。真要优化就上路由级 ",[45,12709,12710],{},"React.lazy",[11,12712,8265],{"id":8265},[75,12714,12716],{"className":1291,"code":12715,"language":1293,"meta":81,"style":81},"$ build-win.bat        # backendmagic-img.exe\n$ bash build-linux.sh  # backendmagic-img（linux\u002Famd64 ELF）\n",[45,12717,12718,12728],{"__ignoreMap":81},[213,12719,12720,12722,12725],{"class":215,"line":216},[213,12721,3387],{"class":237},[213,12723,12724],{"class":377}," build-win.bat",[213,12726,12727],{"class":219},"        # backendmagic-img.exe\n",[213,12729,12730,12732,12734,12737],{"class":215,"line":223},[213,12731,3387],{"class":237},[213,12733,8277],{"class":377},[213,12735,12736],{"class":377}," build-linux.sh",[213,12738,12739],{"class":219},"  # backendmagic-img（linux\u002Famd64 ELF）\n",[15,12741,12742,12743,12746,12747,59],{},"一个约 26MB 的二进制，内嵌 React 前端、Gin API、SQLite 数据初始化，拷到 Windows\u002FLinux 机器直接跑，浏览器访问 ",[45,12744,12745],{},"http:\u002F\u002Flocalhost:8080"," 自动重定向到 ",[45,12748,11487],{},[11,12750,2439],{"id":2439},[15,12752,12753],{},"这轮相比上篇的三点升级：",[26,12755,12756,12762,12772],{},[29,12757,12758,12761],{},[19,12759,12760],{},"hash 路由 + base \u002Fdist","：省掉 SPA fallback，静态服务即开即用；",[29,12763,12764,73,12769,12771],{},[19,12765,12766,12768],{},[45,12767,11349],{}," embed 前缀",[45,12770,8089],{}," 开头文件问题一劳永逸；",[29,12773,12774,12780],{},[19,12775,12776,12777,12779],{},"显式 ",[45,12778,12558],{}," 交叉编译","：让「一个脚本、任意平台、产出 Linux 二进制」成为现实。",[15,12782,12783,12784,12787],{},"加上",[19,12785,12786],{},"入口 no-store \u002F 资源 immutable"," 的缓存策略，单文件分发的体验才算闭环——不然每次升级都要和浏览器缓存打架。",[1231,12789,12790],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":81,"searchDepth":223,"depth":244,"links":12792},[12793,12794,12795,12802,12810,12811],{"id":3144,"depth":223,"text":3144},{"id":7448,"depth":223,"text":7448},{"id":7462,"depth":223,"text":7462,"children":12796},[12797,12798,12799,12800,12801],{"id":11377,"depth":244,"text":11378},{"id":11495,"depth":244,"text":11496},{"id":11551,"depth":244,"text":11552},{"id":11888,"depth":244,"text":11889},{"id":12238,"depth":244,"text":12239},{"id":8049,"depth":223,"text":8049,"children":12803},[12804,12806,12807,12808,12809],{"id":12451,"depth":244,"text":12805},"坑一：\u002F\u002Fgo:embed dist 静默漏掉 _ 开头文件（升级解法）",{"id":12500,"depth":244,"text":12501},{"id":12545,"depth":244,"text":12546},{"id":12640,"depth":244,"text":12641},{"id":12699,"depth":244,"text":12700},{"id":8265,"depth":223,"text":8265},{"id":2439,"depth":223,"text":2439},"以 Magic Img（React 19 + Vite + Gin + SQLite）为实例，记录把前端塞进 Go 二进制、并在 Windows 上交叉编译 Linux 产物的完整过程：embed 的 all 前缀、hash 路由、缓存策略、GOOS 交叉编译等实操坑。","\u002Fposts\u002Fgo-embed-react-single-binary\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-embed-react-single-binary","2026-08-16T00:00:00",{"title":11289,"description":12812},"posts\u002Fgo-embed-react-single-binary\u002Findex",[2076,10370,7501,12820,12821],"交叉编译","打包","WV-T-MOO5Cu-aycw-Ony8s9hjA3_k0S4XU-T0QsCow0",{"id":12824,"title":12825,"author":6,"body":12826,"description":14071,"difficulty":1259,"draft":1260,"extension":1261,"image":14072,"meta":14073,"navigation":666,"path":14074,"pinned":1260,"published":12816,"seo":14075,"series":14076,"seriesOrder":216,"stem":14077,"tags":14078,"updated":1272,"__hash__":14082},"posts\u002Fposts\u002Freact-photo-album-masonry\u002Findex.md","用 react-photo-album 给生图图库做瀑布流排版",{"type":8,"value":12827,"toc":14054},[12828,12831,12834,12852,12859,12923,12934,12937,12960,12963,12967,12981,12985,13001,13179,13183,13580,13583,13617,13620,13624,13634,13640,13665,13670,13681,13687,13698,13701,13741,13754,13877,13889,13893,13900,13918,13923,14013,14019,14023,14034,14037,14040,14046,14051],[11,12829,12830],{"id":12830},"技术背景",[15,12832,12833],{},"Magic Img 是我做的一个 AI 生图提示词管理工具（自托管、本地优先），其中「生图图库」页面要把所有生成图片以图片墙的形式展示出来。图片墙有两个天然矛盾：",[26,12835,12836,12842],{},[29,12837,12838,12841],{},[19,12839,12840],{},"等宽网格","（CSS Grid）：整齐，但每行高度由最高的那张决定——不同比例的图片混排时，矮图下面会留下大洞，看起来\"很乱\"；",[29,12843,12844,12847,12848,12851],{},[19,12845,12846],{},"原生比例展示","：图库必须不裁剪（这是产品约束），所以不能用 ",[45,12849,12850],{},"object-cover"," 强行统一比例。",[15,12853,12854,12855,12858],{},"业内的标准解法是 ",[19,12856,12857],{},"Masonry 瀑布流","：按列紧密堆放，每张图保持原始比例，列间高度错落但整体紧凑。实现瀑布流的常见方案有三类：",[249,12860,12861,12875],{},[252,12862,12863],{},[255,12864,12865,12867,12870,12873],{},[258,12866,3946],{},[258,12868,12869],{},"依赖",[258,12871,12872],{},"顺序",[258,12874,6547],{},[265,12876,12877,12894,12907],{},[255,12878,12879,12886,12888,12891],{},[270,12880,12881,12882,12885],{},"CSS ",[45,12883,12884],{},"columns"," 多列",[270,12887,2661],{},[270,12889,12890],{},"纵向阅读序（先下后右）",[270,12892,12893],{},"最新图片不再出现在左上角，时间语义丢失",[255,12895,12896,12899,12901,12904],{},[270,12897,12898],{},"grid row-span 自研",[270,12900,2661],{},[270,12902,12903],{},"保持",[270,12905,12906],{},"需固定列数 + 固定行高，逻辑要自己写",[255,12908,12909,12914,12917,12920],{},[270,12910,12911],{},[19,12912,12913],{},"react-photo-album",[270,12915,12916],{},"~4KB，无依赖",[270,12918,12919],{},"基本保持（逐列最短列填充）",[270,12921,12922],{},"专为相册墙设计，TS 友好，维护活跃",[15,12924,12925,12926,12929,12930,12933],{},"最终选了 ",[19,12927,12928],{},"react-photo-album v3","：体积小、类型齐全，而且它的 Masonry 模式恰好需要「每张图的 ",[45,12931,12932],{},"width\u002Fheight","」——我们的数据库里本来就存了，零额外成本。",[11,12935,12936],{"id":12936},"需求梳理",[26,12938,12939,12945,12951,12954,12957],{},[29,12940,12941,12942,12633],{},"展示全部生成图（21 张起，会持续增长），",[19,12943,12944],{},"原比例、不裁剪",[29,12946,12947,12948,12633],{},"不同尺寸（2304×1728、4608×3456 等）混排时",[19,12949,12950],{},"紧凑、不留洞",[29,12952,12953],{},"顺序基本保持「新图靠前」；",[29,12955,12956],{},"点击任意一张打开大图预览（YARL 灯箱），悬停显示所属提示词；",[29,12958,12959],{},"平板性能：墙上只用 300px 缩略图，原图只进灯箱按需加载。",[11,12961,12962],{"id":12962},"安装与使用",[113,12964,12966],{"id":12965},"_1-安装","1. 安装",[75,12968,12970],{"className":1291,"code":12969,"language":1293,"meta":81,"style":81},"npm install react-photo-album\n",[45,12971,12972],{"__ignoreMap":81},[213,12973,12974,12976,12978],{"class":215,"line":216},[213,12975,6812],{"class":237},[213,12977,1363],{"class":377},[213,12979,12980],{"class":377}," react-photo-album\n",[113,12982,12984],{"id":12983},"_2-准备数据关键输入是宽高","2. 准备数据（关键输入是宽高）",[15,12986,12987,12988,12991,12992,8072,12994,8072,12997,13000],{},"库要求的 ",[45,12989,12990],{},"Photo"," 对象只有三个必需字段：",[45,12993,3807],{},[45,12995,12996],{},"width",[45,12998,12999],{},"height","。我们的接口本来就返回每张图的原始宽高：",[75,13002,13004],{"className":11435,"code":13003,"language":11437,"meta":81,"style":81},"import PhotoAlbum, { type Photo } from \"react-photo-album\"\n\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\nimport \"react-photo-album\u002Fstyles.css\"\n\ninterface GalleryPhoto extends Photo {\n  prompt_text?: string\n}\n\nconst albumPhotos: GalleryPhoto[] = images.map((img) => ({\n  key: String(img.id),\n  src: assetUrl(img.thumb_path) || assetUrl(img.file_path),\n  width: img.width,\n  height: img.height,\n  alt: img.prompt_text || `生成图 ${img.id}`,\n  prompt_text: img.prompt_text,\n}))\n",[45,13005,13006,13024,13029,13036,13040,13056,13066,13070,13074,13107,13118,13137,13142,13147,13169,13174],{"__ignoreMap":81},[213,13007,13008,13010,13013,13015,13018,13021],{"class":215,"line":216},[213,13009,634],{"class":230},[213,13011,13012],{"class":226}," PhotoAlbum, { ",[213,13014,6069],{"class":230},[213,13016,13017],{"class":226}," Photo } ",[213,13019,13020],{"class":230},"from",[213,13022,13023],{"class":377}," \"react-photo-album\"\n",[213,13025,13026],{"class":215,"line":223},[213,13027,13028],{"class":219},"\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\n",[213,13030,13031,13033],{"class":215,"line":244},[213,13032,634],{"class":230},[213,13034,13035],{"class":377}," \"react-photo-album\u002Fstyles.css\"\n",[213,13037,13038],{"class":215,"line":360},[213,13039,667],{"emptyLinePlaceholder":666},[213,13041,13042,13045,13048,13051,13054],{"class":215,"line":395},[213,13043,13044],{"class":230},"interface",[213,13046,13047],{"class":237}," GalleryPhoto",[213,13049,13050],{"class":230}," extends",[213,13052,13053],{"class":237}," Photo",[213,13055,427],{"class":226},[213,13057,13058,13061,13064],{"class":215,"line":412},[213,13059,13060],{"class":865},"  prompt_text",[213,13062,13063],{"class":230},"?:",[213,13065,6075],{"class":339},[213,13067,13068],{"class":215,"line":430},[213,13069,445],{"class":226},[213,13071,13072],{"class":215,"line":442},[213,13073,667],{"emptyLinePlaceholder":666},[213,13075,13076,13078,13081,13083,13085,13087,13089,13092,13095,13098,13100,13102,13104],{"class":215,"line":448},[213,13077,3503],{"class":230},[213,13079,13080],{"class":339}," albumPhotos",[213,13082,2203],{"class":230},[213,13084,13047],{"class":237},[213,13086,3294],{"class":226},[213,13088,1386],{"class":230},[213,13090,13091],{"class":226}," images.",[213,13093,13094],{"class":237},"map",[213,13096,13097],{"class":226},"((",[213,13099,1540],{"class":865},[213,13101,745],{"class":226},[213,13103,3696],{"class":230},[213,13105,13106],{"class":226}," ({\n",[213,13108,13109,13112,13115],{"class":215,"line":461},[213,13110,13111],{"class":226},"  key: ",[213,13113,13114],{"class":237},"String",[213,13116,13117],{"class":226},"(img.id),\n",[213,13119,13120,13123,13126,13129,13131,13134],{"class":215,"line":751},[213,13121,13122],{"class":226},"  src: ",[213,13124,13125],{"class":237},"assetUrl",[213,13127,13128],{"class":226},"(img.thumb_path) ",[213,13130,3483],{"class":230},[213,13132,13133],{"class":237}," assetUrl",[213,13135,13136],{"class":226},"(img.file_path),\n",[213,13138,13139],{"class":215,"line":765},[213,13140,13141],{"class":226},"  width: img.width,\n",[213,13143,13144],{"class":215,"line":774},[213,13145,13146],{"class":226},"  height: img.height,\n",[213,13148,13149,13152,13154,13157,13159,13161,13164,13167],{"class":215,"line":780},[213,13150,13151],{"class":226},"  alt: img.prompt_text ",[213,13153,3483],{"class":230},[213,13155,13156],{"class":377}," `生成图 ${",[213,13158,1540],{"class":226},[213,13160,686],{"class":377},[213,13162,13163],{"class":226},"id",[213,13165,13166],{"class":377},"}`",[213,13168,2928],{"class":226},[213,13170,13171],{"class":215,"line":797},[213,13172,13173],{"class":226},"  prompt_text: img.prompt_text,\n",[213,13175,13176],{"class":215,"line":810},[213,13177,13178],{"class":226},"}))\n",[113,13180,13182],{"id":13181},"_3-使用-masonry-布局","3. 使用 Masonry 布局",[75,13184,13186],{"className":11435,"code":13185,"language":11437,"meta":81,"style":81},"\u003CPhotoAlbum\n  layout=\"masonry\"\n  columns={(cw) => (cw \u003C 640 ? 2 : cw \u003C 960 ? 3 : cw \u003C 1280 ? 4 : 5)}\n  spacing={16}\n  padding={0}\n  photos={albumPhotos}\n  onClick={({ index }) => openPreview(index)}\n  render={{\n    photo: (props, context) => {\n      const { onClick } = props\n      const { photo, index, width, height } = context\n      return (\n        \u003Cbutton type=\"button\" onClick={onClick} className=\"group relative block w-full ...\">\n          \u003Cimg\n            src={photo.src}\n            alt={photo.alt || \"\"}\n            decoding=\"async\"\n            style={{ width, height }}\n          \u002F>\n          {\u002F* hover 提示词浮层 *\u002F}\n          \u003Cdiv className=\"... opacity-0 group-hover:opacity-100\">\n            {photo.prompt_text}\n          \u003C\u002Fdiv>\n        \u003C\u002Fbutton>\n      )\n    },\n  }}\n\u002F>\n",[45,13187,13188,13195,13205,13273,13287,13300,13310,13334,13344,13365,13383,13411,13418,13452,13460,13470,13486,13496,13506,13511,13521,13538,13543,13552,13561,13566,13570,13575],{"__ignoreMap":81},[213,13189,13190,13192],{"class":215,"line":216},[213,13191,2206],{"class":226},[213,13193,13194],{"class":339},"PhotoAlbum\n",[213,13196,13197,13200,13202],{"class":215,"line":223},[213,13198,13199],{"class":237},"  layout",[213,13201,1386],{"class":230},[213,13203,13204],{"class":377},"\"masonry\"\n",[213,13206,13207,13210,13212,13215,13218,13220,13222,13225,13227,13230,13233,13235,13238,13241,13243,13246,13248,13251,13253,13255,13257,13260,13262,13265,13267,13270],{"class":215,"line":244},[213,13208,13209],{"class":237},"  columns",[213,13211,1386],{"class":230},[213,13213,13214],{"class":226},"{(",[213,13216,13217],{"class":865},"cw",[213,13219,745],{"class":226},[213,13221,3696],{"class":230},[213,13223,13224],{"class":226}," (cw ",[213,13226,2206],{"class":230},[213,13228,13229],{"class":339}," 640",[213,13231,13232],{"class":230}," ?",[213,13234,5869],{"class":339},[213,13236,13237],{"class":230}," :",[213,13239,13240],{"class":226}," cw ",[213,13242,2206],{"class":230},[213,13244,13245],{"class":339}," 960",[213,13247,13232],{"class":230},[213,13249,13250],{"class":339}," 3",[213,13252,13237],{"class":230},[213,13254,13240],{"class":226},[213,13256,2206],{"class":230},[213,13258,13259],{"class":339}," 1280",[213,13261,13232],{"class":230},[213,13263,13264],{"class":339}," 4",[213,13266,13237],{"class":230},[213,13268,13269],{"class":339}," 5",[213,13271,13272],{"class":226},")}\n",[213,13274,13275,13278,13280,13282,13285],{"class":215,"line":360},[213,13276,13277],{"class":237},"  spacing",[213,13279,1386],{"class":230},[213,13281,5536],{"class":226},[213,13283,13284],{"class":339},"16",[213,13286,445],{"class":226},[213,13288,13289,13292,13294,13296,13298],{"class":215,"line":395},[213,13290,13291],{"class":237},"  padding",[213,13293,1386],{"class":230},[213,13295,5536],{"class":226},[213,13297,3359],{"class":339},[213,13299,445],{"class":226},[213,13301,13302,13305,13307],{"class":215,"line":412},[213,13303,13304],{"class":237},"  photos",[213,13306,1386],{"class":230},[213,13308,13309],{"class":226},"{albumPhotos}\n",[213,13311,13312,13315,13317,13320,13323,13326,13328,13331],{"class":215,"line":430},[213,13313,13314],{"class":237},"  onClick",[213,13316,1386],{"class":230},[213,13318,13319],{"class":226},"{({ ",[213,13321,13322],{"class":865},"index",[213,13324,13325],{"class":226}," }) ",[213,13327,3696],{"class":230},[213,13329,13330],{"class":237}," openPreview",[213,13332,13333],{"class":226},"(index)}\n",[213,13335,13336,13339,13341],{"class":215,"line":442},[213,13337,13338],{"class":237},"  render",[213,13340,1386],{"class":230},[213,13342,13343],{"class":226},"{{\n",[213,13345,13346,13349,13352,13354,13356,13359,13361,13363],{"class":215,"line":448},[213,13347,13348],{"class":237},"    photo",[213,13350,13351],{"class":226},": (",[213,13353,3802],{"class":865},[213,13355,381],{"class":226},[213,13357,13358],{"class":865},"context",[213,13360,745],{"class":226},[213,13362,3696],{"class":230},[213,13364,427],{"class":226},[213,13366,13367,13369,13372,13375,13378,13380],{"class":215,"line":461},[213,13368,3408],{"class":230},[213,13370,13371],{"class":226}," { ",[213,13373,13374],{"class":339},"onClick",[213,13376,13377],{"class":226}," } ",[213,13379,1386],{"class":230},[213,13381,13382],{"class":226}," props\n",[213,13384,13385,13387,13389,13392,13394,13396,13398,13400,13402,13404,13406,13408],{"class":215,"line":751},[213,13386,3408],{"class":230},[213,13388,13371],{"class":226},[213,13390,13391],{"class":339},"photo",[213,13393,381],{"class":226},[213,13395,13322],{"class":339},[213,13397,381],{"class":226},[213,13399,12996],{"class":339},[213,13401,381],{"class":226},[213,13403,12999],{"class":339},[213,13405,13377],{"class":226},[213,13407,1386],{"class":230},[213,13409,13410],{"class":226}," context\n",[213,13412,13413,13416],{"class":215,"line":765},[213,13414,13415],{"class":230},"      return",[213,13417,637],{"class":226},[213,13419,13420,13423,13426,13429,13431,13434,13437,13439,13442,13445,13447,13450],{"class":215,"line":774},[213,13421,13422],{"class":226},"        \u003C",[213,13424,13425],{"class":2977},"button",[213,13427,13428],{"class":237}," type",[213,13430,1386],{"class":230},[213,13432,13433],{"class":377},"\"button\"",[213,13435,13436],{"class":237}," onClick",[213,13438,1386],{"class":230},[213,13440,13441],{"class":226},"{onClick} ",[213,13443,13444],{"class":237},"className",[213,13446,1386],{"class":230},[213,13448,13449],{"class":377},"\"group relative block w-full ...\"",[213,13451,1864],{"class":226},[213,13453,13454,13457],{"class":215,"line":780},[213,13455,13456],{"class":226},"          \u003C",[213,13458,13459],{"class":2977},"img\n",[213,13461,13462,13465,13467],{"class":215,"line":797},[213,13463,13464],{"class":237},"            src",[213,13466,1386],{"class":230},[213,13468,13469],{"class":226},"{photo.src}\n",[213,13471,13472,13475,13477,13480,13482,13484],{"class":215,"line":810},[213,13473,13474],{"class":237},"            alt",[213,13476,1386],{"class":230},[213,13478,13479],{"class":226},"{photo.alt ",[213,13481,3483],{"class":230},[213,13483,5780],{"class":377},[213,13485,445],{"class":226},[213,13487,13488,13491,13493],{"class":215,"line":817},[213,13489,13490],{"class":237},"            decoding",[213,13492,1386],{"class":230},[213,13494,13495],{"class":377},"\"async\"\n",[213,13497,13498,13501,13503],{"class":215,"line":822},[213,13499,13500],{"class":237},"            style",[213,13502,1386],{"class":230},[213,13504,13505],{"class":226},"{{ width, height }}\n",[213,13507,13508],{"class":215,"line":834},[213,13509,13510],{"class":226},"          \u002F>\n",[213,13512,13513,13516,13519],{"class":215,"line":842},[213,13514,13515],{"class":226},"          {",[213,13517,13518],{"class":219},"\u002F* hover 提示词浮层 *\u002F",[213,13520,445],{"class":226},[213,13522,13523,13525,13528,13531,13533,13536],{"class":215,"line":1714},[213,13524,13456],{"class":226},[213,13526,13527],{"class":2977},"div",[213,13529,13530],{"class":237}," className",[213,13532,1386],{"class":230},[213,13534,13535],{"class":377},"\"... opacity-0 group-hover:opacity-100\"",[213,13537,1864],{"class":226},[213,13539,13540],{"class":215,"line":1719},[213,13541,13542],{"class":226},"            {photo.prompt_text}\n",[213,13544,13545,13548,13550],{"class":215,"line":2350},[213,13546,13547],{"class":226},"          \u003C\u002F",[213,13549,13527],{"class":2977},[213,13551,1864],{"class":226},[213,13553,13554,13557,13559],{"class":215,"line":2364},[213,13555,13556],{"class":226},"        \u003C\u002F",[213,13558,13425],{"class":2977},[213,13560,1864],{"class":226},[213,13562,13563],{"class":215,"line":2371},[213,13564,13565],{"class":226},"      )\n",[213,13567,13568],{"class":215,"line":2376},[213,13569,8226],{"class":226},[213,13571,13572],{"class":215,"line":2382},[213,13573,13574],{"class":226},"  }}\n",[213,13576,13577],{"class":215,"line":2390},[213,13578,13579],{"class":226},"\u002F>\n",[15,13581,13582],{},"几个要点：",[177,13584,13585,13594,13607],{},[29,13586,13587,13589,13590,13593],{},[45,13588,12884],{}," 可以是数字或 ",[45,13591,13592],{},"(containerWidth) => number"," 函数，做响应式列数很方便；",[29,13595,13596,13597,13599,13600,13603,13604,13606],{},"点击回调 ",[45,13598,13374],{}," 里拿到的是",[19,13601,13602],{},"原始数组下标"," ",[45,13605,13322],{},"，直接传给灯箱即可；",[29,13608,13609,13610,13613,13614,13616],{},"自定义 ",[45,13611,13612],{},"render.photo"," 后，图片尺寸要自己用 context 里的 ",[45,13615,12932],{},"（渲染尺寸）设置——这两个值库已经按列宽算好了。",[11,13618,13619],{"id":13619},"遇到的坑",[113,13621,13623],{"id":13622},"坑-1忘记引入样式表-一张一列","坑 1：忘记引入样式表 → 「一张一列」",[15,13625,13626,13627,13630,13631,59],{},"这是最坑的一个。装完包、写好 ",[45,13628,13629],{},"\u003CPhotoAlbum layout=\"masonry\">","，页面渲染出来却是：",[19,13632,13633],{},"每张图占一整行，右侧大片置灰空白",[15,13635,13636,13637,13639],{},"排查过程：先怀疑是 ",[45,13638,12884],{}," 函数没生效，改成固定数字无效；再检查 DOM——容器和 5 个列（track）都在，列的宽度也算对了，但图片全都堆在布局里。",[15,13641,13642,13643,13646,13647,8072,13650,13653,13654,13660,13661,13664],{},"最后翻包的 ",[45,13644,13645],{},"package.json"," 发现它导出了独立的 CSS 文件（",[45,13648,13649],{},".\u002Fstyles.css",[45,13651,13652],{},".\u002Fmasonry.css"," 等），而",[19,13655,13656,13657],{},"官方 README 示例都带了一行 ",[45,13658,13659],{},"import \"react-photo-album\u002Fstyles.css\"","。容器是 flex、列是 flex-column、列宽按 ",[45,13662,13663],{},"--react-photo-album--columns"," 计算——这些规则全在这个样式表里，不引入就退化成普通块级堆叠。",[15,13666,13667,13669],{},[19,13668,8067],{},"：顶部加一行：",[75,13671,13673],{"className":11435,"code":13672,"language":11437,"meta":81,"style":81},"import \"react-photo-album\u002Fstyles.css\"\n",[45,13674,13675],{"__ignoreMap":81},[213,13676,13677,13679],{"class":215,"line":216},[213,13678,634],{"class":230},[213,13680,13035],{"class":377},[15,13682,13683,13684],{},"经验：",[19,13685,13686],{},"用这类\"无运行时依赖\"的布局库，先看它的 exports 里有没有独立 CSS，有就一定要引。",[113,13688,13690,13691,13693,13694,13697],{"id":13689},"坑-2renderphoto-是-props-context-双参签名","坑 2：",[45,13692,13612],{}," 是 ",[45,13695,13696],{},"(props, context)"," 双参签名",[15,13699,13700],{},"自定义渲染时我凭直觉写了：",[75,13702,13704],{"className":11435,"code":13703,"language":11437,"meta":81,"style":81},"photo: ({ photo, index, width, height, onClick }) => ... \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[45,13705,13706],{"__ignoreMap":81},[213,13707,13708,13710,13713,13715,13717,13719,13721,13723,13725,13727,13729,13731,13733,13735,13738],{"class":215,"line":216},[213,13709,13391],{"class":237},[213,13711,13712],{"class":226},": ({ ",[213,13714,13391],{"class":865},[213,13716,381],{"class":226},[213,13718,13322],{"class":865},[213,13720,381],{"class":226},[213,13722,12996],{"class":865},[213,13724,381],{"class":226},[213,13726,12999],{"class":865},[213,13728,381],{"class":226},[213,13730,13374],{"class":865},[213,13732,13325],{"class":226},[213,13734,3696],{"class":230},[213,13736,13737],{"class":230}," ...",[213,13739,13740],{"class":219}," \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[15,13742,13743,13744,13746,13747,13750,13751,73],{},"TS 报错提示 ",[45,13745,13391],{}," 等属性不存在于 ",[45,13748,13749],{},"RenderPhotoProps","。看类型定义才发现 v3 的渲染函数是",[19,13752,13753],{},"两个参数",[75,13755,13757],{"className":11435,"code":13756,"language":11437,"meta":81,"style":81},"type RenderFunction\u003CProps, Context> = (props: Props, context: Context) => ReactNode\n\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\nphoto: (props, context) => {\n  const { onClick } = props\n  const { photo, index, width, height } = context\n  ...\n}\n",[45,13758,13759,13805,13810,13830,13844,13869,13873],{"__ignoreMap":81},[213,13760,13761,13763,13766,13768,13771,13773,13775,13778,13780,13782,13784,13786,13789,13791,13793,13795,13798,13800,13802],{"class":215,"line":216},[213,13762,6069],{"class":230},[213,13764,13765],{"class":237}," RenderFunction",[213,13767,2206],{"class":226},[213,13769,13770],{"class":237},"Props",[213,13772,381],{"class":226},[213,13774,4897],{"class":237},[213,13776,13777],{"class":226},"> ",[213,13779,1386],{"class":230},[213,13781,3314],{"class":226},[213,13783,3802],{"class":865},[213,13785,2203],{"class":230},[213,13787,13788],{"class":237}," Props",[213,13790,381],{"class":226},[213,13792,13358],{"class":865},[213,13794,2203],{"class":230},[213,13796,13797],{"class":237}," Context",[213,13799,745],{"class":226},[213,13801,3696],{"class":230},[213,13803,13804],{"class":237}," ReactNode\n",[213,13806,13807],{"class":215,"line":223},[213,13808,13809],{"class":219},"\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\n",[213,13811,13812,13814,13816,13818,13820,13822,13824,13826,13828],{"class":215,"line":244},[213,13813,13391],{"class":237},[213,13815,2203],{"class":230},[213,13817,3314],{"class":226},[213,13819,3802],{"class":865},[213,13821,381],{"class":226},[213,13823,13358],{"class":865},[213,13825,745],{"class":226},[213,13827,3696],{"class":230},[213,13829,427],{"class":226},[213,13831,13832,13834,13836,13838,13841],{"class":215,"line":360},[213,13833,3756],{"class":237},[213,13835,13371],{"class":226},[213,13837,13374],{"class":865},[213,13839,13840],{"class":226}," } = ",[213,13842,13843],{"class":865},"props\n",[213,13845,13846,13848,13850,13852,13854,13856,13858,13860,13862,13864,13866],{"class":215,"line":395},[213,13847,3756],{"class":237},[213,13849,13371],{"class":226},[213,13851,13391],{"class":865},[213,13853,381],{"class":226},[213,13855,13322],{"class":865},[213,13857,381],{"class":226},[213,13859,12996],{"class":865},[213,13861,381],{"class":226},[213,13863,12999],{"class":865},[213,13865,13840],{"class":226},[213,13867,13868],{"class":865},"context\n",[213,13870,13871],{"class":215,"line":412},[213,13872,11428],{"class":230},[213,13874,13875],{"class":215,"line":430},[213,13876,445],{"class":226},[15,13878,13879,13881,13882,13884,13885,13888],{},[19,13880,8067],{},"：按 ",[45,13883,13696],{}," 拆解即可。装新库先扫一眼 ",[45,13886,13887],{},".d.ts"," 里的 Render 类型签名，能少走很多弯路。",[113,13890,13892],{"id":13891},"坑-3测试断言把dom-顺序当成了视觉顺序","坑 3：测试断言把「DOM 顺序」当成了「视觉顺序」",[15,13894,13895,13896,13899],{},"布局修好后，我在浏览器自测脚本里加了一条断言：取前两个 ",[45,13897,13898],{},"\u003Cimg>","，比较它们的左坐标，认为应该并排（不同列）。",[15,13901,13902,13903,13906,13907,8106,13910,13913,13914,13917],{},"结果断言失败——但页面明明是 5 列并排。排查发现：",[19,13904,13905],{},"瀑布流的 DOM 是按\"列\"组织的","（第 1 列的全部图片在前，第 2 列的在后），所以 ",[45,13908,13909],{},"img[0]",[45,13911,13912],{},"img[1]"," 属于",[19,13915,13916],{},"同一列","，左坐标当然相同。",[15,13919,13920,13922],{},[19,13921,8067],{},"：断言改为比较「第 1 列的首张图 vs 第 2 列的首张图」：",[75,13924,13928],{"className":13925,"code":13926,"language":13927,"meta":81,"style":81},"language-js shiki shiki-themes github-light github-dark","const a = tracks[0].querySelector(\"img\")\nconst b = tracks[1].querySelector(\"img\")\nreturn Math.abs(a.getBoundingClientRect().left - b.getBoundingClientRect().left) > 10\n","js",[45,13929,13930,13955,13978],{"__ignoreMap":81},[213,13931,13932,13934,13936,13938,13941,13943,13945,13948,13950,13953],{"class":215,"line":216},[213,13933,3503],{"class":230},[213,13935,4512],{"class":339},[213,13937,2915],{"class":230},[213,13939,13940],{"class":226}," tracks[",[213,13942,3359],{"class":339},[213,13944,3423],{"class":226},[213,13946,13947],{"class":237},"querySelector",[213,13949,739],{"class":226},[213,13951,13952],{"class":377},"\"img\"",[213,13954,392],{"class":226},[213,13956,13957,13959,13962,13964,13966,13968,13970,13972,13974,13976],{"class":215,"line":223},[213,13958,3503],{"class":230},[213,13960,13961],{"class":339}," b",[213,13963,2915],{"class":230},[213,13965,13940],{"class":226},[213,13967,1404],{"class":339},[213,13969,3423],{"class":226},[213,13971,13947],{"class":237},[213,13973,739],{"class":226},[213,13975,13952],{"class":377},[213,13977,392],{"class":226},[213,13979,13980,13982,13985,13988,13991,13994,13997,14000,14003,14005,14008,14010],{"class":215,"line":244},[213,13981,3323],{"class":230},[213,13983,13984],{"class":226}," Math.",[213,13986,13987],{"class":237},"abs",[213,13989,13990],{"class":226},"(a.",[213,13992,13993],{"class":237},"getBoundingClientRect",[213,13995,13996],{"class":226},"().left ",[213,13998,13999],{"class":230},"-",[213,14001,14002],{"class":226}," b.",[213,14004,13993],{"class":237},[213,14006,14007],{"class":226},"().left) ",[213,14009,1817],{"class":230},[213,14011,14012],{"class":339}," 10\n",[15,14014,13683,14015,14018],{},[19,14016,14017],{},"验证布局类库时，先搞清楚它生成的 DOM 结构，再写断言","；另外把「真实浏览器探针」当排查手段（直接打印各 track 的 x 坐标和子元素数量），比盯着代码猜快得多。",[113,14020,14022],{"id":14021},"坑-4等宽网格的洞引入前就存在的产品问题","坑 4：等宽网格的\"洞\"（引入前就存在的产品问题）",[15,14024,14025,14026,14029,14030,14033],{},"顺带记录一下为什么换掉原来的布局：CSS Grid ",[45,14027,14028],{},"repeat(auto-fill, minmax(220px, 1fr))"," 等宽铺满，图片 ",[45,14031,14032],{},"w-full h-auto","。当一行里混有不同比例时，行高取最高的那张，矮图底部留空，几行错落下来就显得乱；另外窗口宽度\"差一点放不下下一张\"时，右侧会空出近一整列。",[15,14035,14036],{},"换成 Masonry 后这两个问题都消失了——列内紧密堆叠、列数自适应。",[11,14038,14039],{"id":14039},"效果与小结",[15,14041,14042,14043,14045],{},"最终效果：21 张不同比例的生成图在 5 列瀑布流里紧密排列，原比例不裁剪，悬停出提示词浮层，点击进 YARL 大图预览；缩略图总大小仅 360KB，进页面即全量加载 + ",[45,14044,12536],{}," 长缓存，平板上快速滚动也跟手。",[15,14047,1197,14048],{},[19,14049,14050],{},"react-photo-album 是个小但专业的相册布局库，只要记住三件事——引样式表、喂宽高、render 函数是双参——就能在十分钟内把图片墙升级成真正的瀑布流。",[1231,14052,14053],{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-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 .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":81,"searchDepth":223,"depth":244,"links":14055},[14056,14057,14058,14063,14070],{"id":12830,"depth":223,"text":12830},{"id":12936,"depth":223,"text":12936},{"id":12962,"depth":223,"text":12962,"children":14059},[14060,14061,14062],{"id":12965,"depth":244,"text":12966},{"id":12983,"depth":244,"text":12984},{"id":13181,"depth":244,"text":13182},{"id":13619,"depth":223,"text":13619,"children":14064},[14065,14066,14068,14069],{"id":13622,"depth":244,"text":13623},{"id":13689,"depth":244,"text":14067},"坑 2：render.photo 是 (props, context) 双参签名",{"id":13891,"depth":244,"text":13892},{"id":14021,"depth":244,"text":14022},{"id":14039,"depth":223,"text":14039},"记录在 Magic Img 生图图库中引入 react-photo-album 实现 Masonry 瀑布流的过程：技术选型背景、需求梳理、安装使用，以及「忘记引入样式表导致一张一列」等踩坑与排查方法。","\u002Fposts\u002Freact-photo-album-masonry\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Freact-photo-album-masonry",{"title":12825,"description":14071},"前端工程实践","posts\u002Freact-photo-album-masonry\u002Findex",[10370,12913,14079,14080,14081],"瀑布流","前端","Tailwind","-O-kAQLEyRzwVhOpuo-yBxZim9LtvhChPkpP92ipyjM",1786887348660]