[{"data":1,"prerenderedAt":14147},["ShallowReactive",2],{"post-react-photo-album-masonry":3,"series-source-react-photo-album-masonry":1384},{"id":4,"title":5,"author":6,"body":7,"description":1366,"difficulty":1367,"draft":1368,"extension":1369,"image":1370,"meta":1371,"navigation":262,"path":1372,"pinned":1368,"published":1373,"seo":1374,"series":1375,"seriesOrder":179,"stem":1376,"tags":1377,"updated":1382,"__hash__":1383},"posts\u002Fposts\u002Freact-photo-album-masonry\u002Findex.md","用 react-photo-album 给生图图库做瀑布流排版",null,{"type":8,"value":9,"toc":1349},"minimark",[10,14,18,40,47,120,131,134,158,161,166,191,195,213,430,434,855,858,893,896,900,911,917,942,948,959,965,976,979,1019,1033,1160,1172,1176,1183,1202,1207,1305,1311,1315,1326,1329,1332,1339,1345],[11,12,13],"h2",{"id":13},"技术背景",[15,16,17],"p",{},"Magic Img 是我做的一个 AI 生图提示词管理工具（自托管、本地优先），其中「生图图库」页面要把所有生成图片以图片墙的形式展示出来。图片墙有两个天然矛盾：",[19,20,21,29],"ol",{},[22,23,24,28],"li",{},[25,26,27],"strong",{},"等宽网格","（CSS Grid）：整齐，但每行高度由最高的那张决定——不同比例的图片混排时，矮图下面会留下大洞，看起来\"很乱\"；",[22,30,31,34,35,39],{},[25,32,33],{},"原生比例展示","：图库必须不裁剪（这是产品约束），所以不能用 ",[36,37,38],"code",{},"object-cover"," 强行统一比例。",[15,41,42,43,46],{},"业内的标准解法是 ",[25,44,45],{},"Masonry 瀑布流","：按列紧密堆放，每张图保持原始比例，列间高度错落但整体紧凑。实现瀑布流的常见方案有三类：",[48,49,50,69],"table",{},[51,52,53],"thead",{},[54,55,56,60,63,66],"tr",{},[57,58,59],"th",{},"方案",[57,61,62],{},"依赖",[57,64,65],{},"顺序",[57,67,68],{},"说明",[70,71,72,91,104],"tbody",{},[54,73,74,82,85,88],{},[75,76,77,78,81],"td",{},"CSS ",[36,79,80],{},"columns"," 多列",[75,83,84],{},"无",[75,86,87],{},"纵向阅读序（先下后右）",[75,89,90],{},"最新图片不再出现在左上角，时间语义丢失",[54,92,93,96,98,101],{},[75,94,95],{},"grid row-span 自研",[75,97,84],{},[75,99,100],{},"保持",[75,102,103],{},"需固定列数 + 固定行高，逻辑要自己写",[54,105,106,111,114,117],{},[75,107,108],{},[25,109,110],{},"react-photo-album",[75,112,113],{},"~4KB，无依赖",[75,115,116],{},"基本保持（逐列最短列填充）",[75,118,119],{},"专为相册墙设计，TS 友好，维护活跃",[15,121,122,123,126,127,130],{},"最终选了 ",[25,124,125],{},"react-photo-album v3","：体积小、类型齐全，而且它的 Masonry 模式恰好需要「每张图的 ",[36,128,129],{},"width\u002Fheight","」——我们的数据库里本来就存了，零额外成本。",[11,132,133],{"id":133},"需求梳理",[19,135,136,143,149,152,155],{},[22,137,138,139,142],{},"展示全部生成图（21 张起，会持续增长），",[25,140,141],{},"原比例、不裁剪","；",[22,144,145,146,142],{},"不同尺寸（2304×1728、4608×3456 等）混排时",[25,147,148],{},"紧凑、不留洞",[22,150,151],{},"顺序基本保持「新图靠前」；",[22,153,154],{},"点击任意一张打开大图预览（YARL 灯箱），悬停显示所属提示词；",[22,156,157],{},"平板性能：墙上只用 300px 缩略图，原图只进灯箱按需加载。",[11,159,160],{"id":160},"安装与使用",[162,163,165],"h3",{"id":164},"_1-安装","1. 安装",[167,168,173],"pre",{"className":169,"code":170,"language":171,"meta":172,"style":172},"language-bash shiki shiki-themes github-light github-dark","npm install react-photo-album\n","bash","",[36,174,175],{"__ignoreMap":172},[176,177,180,184,188],"span",{"class":178,"line":179},"line",1,[176,181,183],{"class":182},"sw2iP","npm",[176,185,187],{"class":186},"scXbn"," install",[176,189,190],{"class":186}," react-photo-album\n",[162,192,194],{"id":193},"_2-准备数据关键输入是宽高","2. 准备数据（关键输入是宽高）",[15,196,197,198,201,202,205,206,205,209,212],{},"库要求的 ",[36,199,200],{},"Photo"," 对象只有三个必需字段：",[36,203,204],{},"src","、",[36,207,208],{},"width",[36,210,211],{},"height","。我们的接口本来就返回每张图的原始宽高：",[167,214,218],{"className":215,"code":216,"language":217,"meta":172,"style":172},"language-tsx shiki shiki-themes github-light github-dark","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","tsx",[36,219,220,242,249,257,264,282,296,302,307,348,360,381,387,393,418,424],{"__ignoreMap":172},[176,221,222,226,230,233,236,239],{"class":178,"line":179},[176,223,225],{"class":224},"siTax","import",[176,227,229],{"class":228},"sIX_F"," PhotoAlbum, { ",[176,231,232],{"class":224},"type",[176,234,235],{"class":228}," Photo } ",[176,237,238],{"class":224},"from",[176,240,241],{"class":186}," \"react-photo-album\"\n",[176,243,245],{"class":178,"line":244},2,[176,246,248],{"class":247},"sHbNN","\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\n",[176,250,252,254],{"class":178,"line":251},3,[176,253,225],{"class":224},[176,255,256],{"class":186}," \"react-photo-album\u002Fstyles.css\"\n",[176,258,260],{"class":178,"line":259},4,[176,261,263],{"emptyLinePlaceholder":262},true,"\n",[176,265,267,270,273,276,279],{"class":178,"line":266},5,[176,268,269],{"class":224},"interface",[176,271,272],{"class":182}," GalleryPhoto",[176,274,275],{"class":224}," extends",[176,277,278],{"class":182}," Photo",[176,280,281],{"class":228}," {\n",[176,283,285,289,292],{"class":178,"line":284},6,[176,286,288],{"class":287},"sPK6S","  prompt_text",[176,290,291],{"class":224},"?:",[176,293,295],{"class":294},"suQ91"," string\n",[176,297,299],{"class":178,"line":298},7,[176,300,301],{"class":228},"}\n",[176,303,305],{"class":178,"line":304},8,[176,306,263],{"emptyLinePlaceholder":262},[176,308,310,313,316,319,321,324,327,330,333,336,339,342,345],{"class":178,"line":309},9,[176,311,312],{"class":224},"const",[176,314,315],{"class":294}," albumPhotos",[176,317,318],{"class":224},":",[176,320,272],{"class":182},[176,322,323],{"class":228},"[] ",[176,325,326],{"class":224},"=",[176,328,329],{"class":228}," images.",[176,331,332],{"class":182},"map",[176,334,335],{"class":228},"((",[176,337,338],{"class":287},"img",[176,340,341],{"class":228},") ",[176,343,344],{"class":224},"=>",[176,346,347],{"class":228}," ({\n",[176,349,351,354,357],{"class":178,"line":350},10,[176,352,353],{"class":228},"  key: ",[176,355,356],{"class":182},"String",[176,358,359],{"class":228},"(img.id),\n",[176,361,363,366,369,372,375,378],{"class":178,"line":362},11,[176,364,365],{"class":228},"  src: ",[176,367,368],{"class":182},"assetUrl",[176,370,371],{"class":228},"(img.thumb_path) ",[176,373,374],{"class":224},"||",[176,376,377],{"class":182}," assetUrl",[176,379,380],{"class":228},"(img.file_path),\n",[176,382,384],{"class":178,"line":383},12,[176,385,386],{"class":228},"  width: img.width,\n",[176,388,390],{"class":178,"line":389},13,[176,391,392],{"class":228},"  height: img.height,\n",[176,394,396,399,401,404,406,409,412,415],{"class":178,"line":395},14,[176,397,398],{"class":228},"  alt: img.prompt_text ",[176,400,374],{"class":224},[176,402,403],{"class":186}," `生成图 ${",[176,405,338],{"class":228},[176,407,408],{"class":186},".",[176,410,411],{"class":228},"id",[176,413,414],{"class":186},"}`",[176,416,417],{"class":228},",\n",[176,419,421],{"class":178,"line":420},15,[176,422,423],{"class":228},"  prompt_text: img.prompt_text,\n",[176,425,427],{"class":178,"line":426},16,[176,428,429],{"class":228},"}))\n",[162,431,433],{"id":432},"_3-使用-masonry-布局","3. 使用 Masonry 布局",[167,435,437],{"className":215,"code":436,"language":217,"meta":172,"style":172},"\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",[36,438,439,447,457,526,541,555,565,589,599,622,641,669,677,713,721,731,748,759,770,776,787,805,811,821,831,837,843,849],{"__ignoreMap":172},[176,440,441,444],{"class":178,"line":179},[176,442,443],{"class":228},"\u003C",[176,445,446],{"class":294},"PhotoAlbum\n",[176,448,449,452,454],{"class":178,"line":244},[176,450,451],{"class":182},"  layout",[176,453,326],{"class":224},[176,455,456],{"class":186},"\"masonry\"\n",[176,458,459,462,464,467,470,472,474,477,479,482,485,488,491,494,496,499,501,504,506,508,510,513,515,518,520,523],{"class":178,"line":251},[176,460,461],{"class":182},"  columns",[176,463,326],{"class":224},[176,465,466],{"class":228},"{(",[176,468,469],{"class":287},"cw",[176,471,341],{"class":228},[176,473,344],{"class":224},[176,475,476],{"class":228}," (cw ",[176,478,443],{"class":224},[176,480,481],{"class":294}," 640",[176,483,484],{"class":224}," ?",[176,486,487],{"class":294}," 2",[176,489,490],{"class":224}," :",[176,492,493],{"class":228}," cw ",[176,495,443],{"class":224},[176,497,498],{"class":294}," 960",[176,500,484],{"class":224},[176,502,503],{"class":294}," 3",[176,505,490],{"class":224},[176,507,493],{"class":228},[176,509,443],{"class":224},[176,511,512],{"class":294}," 1280",[176,514,484],{"class":224},[176,516,517],{"class":294}," 4",[176,519,490],{"class":224},[176,521,522],{"class":294}," 5",[176,524,525],{"class":228},")}\n",[176,527,528,531,533,536,539],{"class":178,"line":259},[176,529,530],{"class":182},"  spacing",[176,532,326],{"class":224},[176,534,535],{"class":228},"{",[176,537,538],{"class":294},"16",[176,540,301],{"class":228},[176,542,543,546,548,550,553],{"class":178,"line":266},[176,544,545],{"class":182},"  padding",[176,547,326],{"class":224},[176,549,535],{"class":228},[176,551,552],{"class":294},"0",[176,554,301],{"class":228},[176,556,557,560,562],{"class":178,"line":284},[176,558,559],{"class":182},"  photos",[176,561,326],{"class":224},[176,563,564],{"class":228},"{albumPhotos}\n",[176,566,567,570,572,575,578,581,583,586],{"class":178,"line":298},[176,568,569],{"class":182},"  onClick",[176,571,326],{"class":224},[176,573,574],{"class":228},"{({ ",[176,576,577],{"class":287},"index",[176,579,580],{"class":228}," }) ",[176,582,344],{"class":224},[176,584,585],{"class":182}," openPreview",[176,587,588],{"class":228},"(index)}\n",[176,590,591,594,596],{"class":178,"line":304},[176,592,593],{"class":182},"  render",[176,595,326],{"class":224},[176,597,598],{"class":228},"{{\n",[176,600,601,604,607,610,613,616,618,620],{"class":178,"line":309},[176,602,603],{"class":182},"    photo",[176,605,606],{"class":228},": (",[176,608,609],{"class":287},"props",[176,611,612],{"class":228},", ",[176,614,615],{"class":287},"context",[176,617,341],{"class":228},[176,619,344],{"class":224},[176,621,281],{"class":228},[176,623,624,627,630,633,636,638],{"class":178,"line":350},[176,625,626],{"class":224},"      const",[176,628,629],{"class":228}," { ",[176,631,632],{"class":294},"onClick",[176,634,635],{"class":228}," } ",[176,637,326],{"class":224},[176,639,640],{"class":228}," props\n",[176,642,643,645,647,650,652,654,656,658,660,662,664,666],{"class":178,"line":362},[176,644,626],{"class":224},[176,646,629],{"class":228},[176,648,649],{"class":294},"photo",[176,651,612],{"class":228},[176,653,577],{"class":294},[176,655,612],{"class":228},[176,657,208],{"class":294},[176,659,612],{"class":228},[176,661,211],{"class":294},[176,663,635],{"class":228},[176,665,326],{"class":224},[176,667,668],{"class":228}," context\n",[176,670,671,674],{"class":178,"line":383},[176,672,673],{"class":224},"      return",[176,675,676],{"class":228}," (\n",[176,678,679,682,686,689,691,694,697,699,702,705,707,710],{"class":178,"line":389},[176,680,681],{"class":228},"        \u003C",[176,683,685],{"class":684},"sbB4o","button",[176,687,688],{"class":182}," type",[176,690,326],{"class":224},[176,692,693],{"class":186},"\"button\"",[176,695,696],{"class":182}," onClick",[176,698,326],{"class":224},[176,700,701],{"class":228},"{onClick} ",[176,703,704],{"class":182},"className",[176,706,326],{"class":224},[176,708,709],{"class":186},"\"group relative block w-full ...\"",[176,711,712],{"class":228},">\n",[176,714,715,718],{"class":178,"line":395},[176,716,717],{"class":228},"          \u003C",[176,719,720],{"class":684},"img\n",[176,722,723,726,728],{"class":178,"line":420},[176,724,725],{"class":182},"            src",[176,727,326],{"class":224},[176,729,730],{"class":228},"{photo.src}\n",[176,732,733,736,738,741,743,746],{"class":178,"line":426},[176,734,735],{"class":182},"            alt",[176,737,326],{"class":224},[176,739,740],{"class":228},"{photo.alt ",[176,742,374],{"class":224},[176,744,745],{"class":186}," \"\"",[176,747,301],{"class":228},[176,749,751,754,756],{"class":178,"line":750},17,[176,752,753],{"class":182},"            decoding",[176,755,326],{"class":224},[176,757,758],{"class":186},"\"async\"\n",[176,760,762,765,767],{"class":178,"line":761},18,[176,763,764],{"class":182},"            style",[176,766,326],{"class":224},[176,768,769],{"class":228},"{{ width, height }}\n",[176,771,773],{"class":178,"line":772},19,[176,774,775],{"class":228},"          \u002F>\n",[176,777,779,782,785],{"class":178,"line":778},20,[176,780,781],{"class":228},"          {",[176,783,784],{"class":247},"\u002F* hover 提示词浮层 *\u002F",[176,786,301],{"class":228},[176,788,790,792,795,798,800,803],{"class":178,"line":789},21,[176,791,717],{"class":228},[176,793,794],{"class":684},"div",[176,796,797],{"class":182}," className",[176,799,326],{"class":224},[176,801,802],{"class":186},"\"... opacity-0 group-hover:opacity-100\"",[176,804,712],{"class":228},[176,806,808],{"class":178,"line":807},22,[176,809,810],{"class":228},"            {photo.prompt_text}\n",[176,812,814,817,819],{"class":178,"line":813},23,[176,815,816],{"class":228},"          \u003C\u002F",[176,818,794],{"class":684},[176,820,712],{"class":228},[176,822,824,827,829],{"class":178,"line":823},24,[176,825,826],{"class":228},"        \u003C\u002F",[176,828,685],{"class":684},[176,830,712],{"class":228},[176,832,834],{"class":178,"line":833},25,[176,835,836],{"class":228},"      )\n",[176,838,840],{"class":178,"line":839},26,[176,841,842],{"class":228},"    },\n",[176,844,846],{"class":178,"line":845},27,[176,847,848],{"class":228},"  }}\n",[176,850,852],{"class":178,"line":851},28,[176,853,854],{"class":228},"\u002F>\n",[15,856,857],{},"几个要点：",[859,860,861,870,883],"ul",{},[22,862,863,865,866,869],{},[36,864,80],{}," 可以是数字或 ",[36,867,868],{},"(containerWidth) => number"," 函数，做响应式列数很方便；",[22,871,872,873,875,876,879,880,882],{},"点击回调 ",[36,874,632],{}," 里拿到的是",[25,877,878],{},"原始数组下标"," ",[36,881,577],{},"，直接传给灯箱即可；",[22,884,885,886,889,890,892],{},"自定义 ",[36,887,888],{},"render.photo"," 后，图片尺寸要自己用 context 里的 ",[36,891,129],{},"（渲染尺寸）设置——这两个值库已经按列宽算好了。",[11,894,895],{"id":895},"遇到的坑",[162,897,899],{"id":898},"坑-1忘记引入样式表-一张一列","坑 1：忘记引入样式表 → 「一张一列」",[15,901,902,903,906,907,910],{},"这是最坑的一个。装完包、写好 ",[36,904,905],{},"\u003CPhotoAlbum layout=\"masonry\">","，页面渲染出来却是：",[25,908,909],{},"每张图占一整行，右侧大片置灰空白","。",[15,912,913,914,916],{},"排查过程：先怀疑是 ",[36,915,80],{}," 函数没生效，改成固定数字无效；再检查 DOM——容器和 5 个列（track）都在，列的宽度也算对了，但图片全都堆在布局里。",[15,918,919,920,923,924,205,927,930,931,937,938,941],{},"最后翻包的 ",[36,921,922],{},"package.json"," 发现它导出了独立的 CSS 文件（",[36,925,926],{},".\u002Fstyles.css",[36,928,929],{},".\u002Fmasonry.css"," 等），而",[25,932,933,934],{},"官方 README 示例都带了一行 ",[36,935,936],{},"import \"react-photo-album\u002Fstyles.css\"","。容器是 flex、列是 flex-column、列宽按 ",[36,939,940],{},"--react-photo-album--columns"," 计算——这些规则全在这个样式表里，不引入就退化成普通块级堆叠。",[15,943,944,947],{},[25,945,946],{},"解决","：顶部加一行：",[167,949,951],{"className":215,"code":950,"language":217,"meta":172,"style":172},"import \"react-photo-album\u002Fstyles.css\"\n",[36,952,953],{"__ignoreMap":172},[176,954,955,957],{"class":178,"line":179},[176,956,225],{"class":224},[176,958,256],{"class":186},[15,960,961,962],{},"经验：",[25,963,964],{},"用这类\"无运行时依赖\"的布局库，先看它的 exports 里有没有独立 CSS，有就一定要引。",[162,966,968,969,971,972,975],{"id":967},"坑-2renderphoto-是-props-context-双参签名","坑 2：",[36,970,888],{}," 是 ",[36,973,974],{},"(props, context)"," 双参签名",[15,977,978],{},"自定义渲染时我凭直觉写了：",[167,980,982],{"className":215,"code":981,"language":217,"meta":172,"style":172},"photo: ({ photo, index, width, height, onClick }) => ... \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[36,983,984],{"__ignoreMap":172},[176,985,986,988,991,993,995,997,999,1001,1003,1005,1007,1009,1011,1013,1016],{"class":178,"line":179},[176,987,649],{"class":182},[176,989,990],{"class":228},": ({ ",[176,992,649],{"class":287},[176,994,612],{"class":228},[176,996,577],{"class":287},[176,998,612],{"class":228},[176,1000,208],{"class":287},[176,1002,612],{"class":228},[176,1004,211],{"class":287},[176,1006,612],{"class":228},[176,1008,632],{"class":287},[176,1010,580],{"class":228},[176,1012,344],{"class":224},[176,1014,1015],{"class":224}," ...",[176,1017,1018],{"class":247}," \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[15,1020,1021,1022,1024,1025,1028,1029,1032],{},"TS 报错提示 ",[36,1023,649],{}," 等属性不存在于 ",[36,1026,1027],{},"RenderPhotoProps","。看类型定义才发现 v3 的渲染函数是",[25,1030,1031],{},"两个参数","：",[167,1034,1036],{"className":215,"code":1035,"language":217,"meta":172,"style":172},"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",[36,1037,1038,1086,1091,1111,1126,1151,1156],{"__ignoreMap":172},[176,1039,1040,1042,1045,1047,1050,1052,1055,1058,1060,1063,1065,1067,1070,1072,1074,1076,1079,1081,1083],{"class":178,"line":179},[176,1041,232],{"class":224},[176,1043,1044],{"class":182}," RenderFunction",[176,1046,443],{"class":228},[176,1048,1049],{"class":182},"Props",[176,1051,612],{"class":228},[176,1053,1054],{"class":182},"Context",[176,1056,1057],{"class":228},"> ",[176,1059,326],{"class":224},[176,1061,1062],{"class":228}," (",[176,1064,609],{"class":287},[176,1066,318],{"class":224},[176,1068,1069],{"class":182}," Props",[176,1071,612],{"class":228},[176,1073,615],{"class":287},[176,1075,318],{"class":224},[176,1077,1078],{"class":182}," Context",[176,1080,341],{"class":228},[176,1082,344],{"class":224},[176,1084,1085],{"class":182}," ReactNode\n",[176,1087,1088],{"class":178,"line":244},[176,1089,1090],{"class":247},"\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\n",[176,1092,1093,1095,1097,1099,1101,1103,1105,1107,1109],{"class":178,"line":251},[176,1094,649],{"class":182},[176,1096,318],{"class":224},[176,1098,1062],{"class":228},[176,1100,609],{"class":287},[176,1102,612],{"class":228},[176,1104,615],{"class":287},[176,1106,341],{"class":228},[176,1108,344],{"class":224},[176,1110,281],{"class":228},[176,1112,1113,1116,1118,1120,1123],{"class":178,"line":259},[176,1114,1115],{"class":182},"  const",[176,1117,629],{"class":228},[176,1119,632],{"class":287},[176,1121,1122],{"class":228}," } = ",[176,1124,1125],{"class":287},"props\n",[176,1127,1128,1130,1132,1134,1136,1138,1140,1142,1144,1146,1148],{"class":178,"line":266},[176,1129,1115],{"class":182},[176,1131,629],{"class":228},[176,1133,649],{"class":287},[176,1135,612],{"class":228},[176,1137,577],{"class":287},[176,1139,612],{"class":228},[176,1141,208],{"class":287},[176,1143,612],{"class":228},[176,1145,211],{"class":287},[176,1147,1122],{"class":228},[176,1149,1150],{"class":287},"context\n",[176,1152,1153],{"class":178,"line":284},[176,1154,1155],{"class":224},"  ...\n",[176,1157,1158],{"class":178,"line":298},[176,1159,301],{"class":228},[15,1161,1162,1164,1165,1167,1168,1171],{},[25,1163,946],{},"：按 ",[36,1166,974],{}," 拆解即可。装新库先扫一眼 ",[36,1169,1170],{},".d.ts"," 里的 Render 类型签名，能少走很多弯路。",[162,1173,1175],{"id":1174},"坑-3测试断言把dom-顺序当成了视觉顺序","坑 3：测试断言把「DOM 顺序」当成了「视觉顺序」",[15,1177,1178,1179,1182],{},"布局修好后，我在浏览器自测脚本里加了一条断言：取前两个 ",[36,1180,1181],{},"\u003Cimg>","，比较它们的左坐标，认为应该并排（不同列）。",[15,1184,1185,1186,1189,1190,1193,1194,1197,1198,1201],{},"结果断言失败——但页面明明是 5 列并排。排查发现：",[25,1187,1188],{},"瀑布流的 DOM 是按\"列\"组织的","（第 1 列的全部图片在前，第 2 列的在后），所以 ",[36,1191,1192],{},"img[0]"," 和 ",[36,1195,1196],{},"img[1]"," 属于",[25,1199,1200],{},"同一列","，左坐标当然相同。",[15,1203,1204,1206],{},[25,1205,946],{},"：断言改为比较「第 1 列的首张图 vs 第 2 列的首张图」：",[167,1208,1212],{"className":1209,"code":1210,"language":1211,"meta":172,"style":172},"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",[36,1213,1214,1244,1268],{"__ignoreMap":172},[176,1215,1216,1218,1221,1224,1227,1229,1232,1235,1238,1241],{"class":178,"line":179},[176,1217,312],{"class":224},[176,1219,1220],{"class":294}," a",[176,1222,1223],{"class":224}," =",[176,1225,1226],{"class":228}," tracks[",[176,1228,552],{"class":294},[176,1230,1231],{"class":228},"].",[176,1233,1234],{"class":182},"querySelector",[176,1236,1237],{"class":228},"(",[176,1239,1240],{"class":186},"\"img\"",[176,1242,1243],{"class":228},")\n",[176,1245,1246,1248,1251,1253,1255,1258,1260,1262,1264,1266],{"class":178,"line":244},[176,1247,312],{"class":224},[176,1249,1250],{"class":294}," b",[176,1252,1223],{"class":224},[176,1254,1226],{"class":228},[176,1256,1257],{"class":294},"1",[176,1259,1231],{"class":228},[176,1261,1234],{"class":182},[176,1263,1237],{"class":228},[176,1265,1240],{"class":186},[176,1267,1243],{"class":228},[176,1269,1270,1273,1276,1279,1282,1285,1288,1291,1294,1296,1299,1302],{"class":178,"line":251},[176,1271,1272],{"class":224},"return",[176,1274,1275],{"class":228}," Math.",[176,1277,1278],{"class":182},"abs",[176,1280,1281],{"class":228},"(a.",[176,1283,1284],{"class":182},"getBoundingClientRect",[176,1286,1287],{"class":228},"().left ",[176,1289,1290],{"class":224},"-",[176,1292,1293],{"class":228}," b.",[176,1295,1284],{"class":182},[176,1297,1298],{"class":228},"().left) ",[176,1300,1301],{"class":224},">",[176,1303,1304],{"class":294}," 10\n",[15,1306,961,1307,1310],{},[25,1308,1309],{},"验证布局类库时，先搞清楚它生成的 DOM 结构，再写断言","；另外把「真实浏览器探针」当排查手段（直接打印各 track 的 x 坐标和子元素数量），比盯着代码猜快得多。",[162,1312,1314],{"id":1313},"坑-4等宽网格的洞引入前就存在的产品问题","坑 4：等宽网格的\"洞\"（引入前就存在的产品问题）",[15,1316,1317,1318,1321,1322,1325],{},"顺带记录一下为什么换掉原来的布局：CSS Grid ",[36,1319,1320],{},"repeat(auto-fill, minmax(220px, 1fr))"," 等宽铺满，图片 ",[36,1323,1324],{},"w-full h-auto","。当一行里混有不同比例时，行高取最高的那张，矮图底部留空，几行错落下来就显得乱；另外窗口宽度\"差一点放不下下一张\"时，右侧会空出近一整列。",[15,1327,1328],{},"换成 Masonry 后这两个问题都消失了——列内紧密堆叠、列数自适应。",[11,1330,1331],{"id":1331},"效果与小结",[15,1333,1334,1335,1338],{},"最终效果：21 张不同比例的生成图在 5 列瀑布流里紧密排列，原比例不裁剪，悬停出提示词浮层，点击进 YARL 大图预览；缩略图总大小仅 360KB，进页面即全量加载 + ",[36,1336,1337],{},"immutable"," 长缓存，平板上快速滚动也跟手。",[15,1340,1341,1342],{},"一句话总结：",[25,1343,1344],{},"react-photo-album 是个小但专业的相册布局库，只要记住三件事——引样式表、喂宽高、render 函数是双参——就能在十分钟内把图片墙升级成真正的瀑布流。",[1346,1347,1348],"style",{},"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":172,"searchDepth":244,"depth":251,"links":1350},[1351,1352,1353,1358,1365],{"id":13,"depth":244,"text":13},{"id":133,"depth":244,"text":133},{"id":160,"depth":244,"text":160,"children":1354},[1355,1356,1357],{"id":164,"depth":251,"text":165},{"id":193,"depth":251,"text":194},{"id":432,"depth":251,"text":433},{"id":895,"depth":244,"text":895,"children":1359},[1360,1361,1363,1364],{"id":898,"depth":251,"text":899},{"id":967,"depth":251,"text":1362},"坑 2：render.photo 是 (props, context) 双参签名",{"id":1174,"depth":251,"text":1175},{"id":1313,"depth":251,"text":1314},{"id":1331,"depth":244,"text":1331},"记录在 Magic Img 生图图库中引入 react-photo-album 实现 Masonry 瀑布流的过程：技术选型背景、需求梳理、安装使用，以及「忘记引入样式表导致一张一列」等踩坑与排查方法。","入门",false,"md","\u002Fposts\u002Freact-photo-album-masonry\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Freact-photo-album-masonry","2026-08-16T00:00:00",{"title":5,"description":1366},"前端工程实践","posts\u002Freact-photo-album-masonry\u002Findex",[1378,110,1379,1380,1381],"React","瀑布流","前端","Tailwind","2026-08-16","-O-kAQLEyRzwVhOpuo-yBxZim9LtvhChPkpP92ipyjM",[1385,2182,2594,2816,3229,4401,4678,6738,7519,8494,9903,10462,11639,13169],{"id":1386,"title":1387,"author":6,"body":1388,"description":2168,"difficulty":2169,"draft":1368,"extension":1369,"image":2170,"meta":2171,"navigation":262,"path":2172,"pinned":1368,"published":2173,"seo":2174,"series":2175,"seriesOrder":179,"stem":2176,"tags":2177,"updated":2173,"__hash__":2181},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":8,"value":1389,"toc":2157},[1390,1393,1396,1399,1620,1623,1664,1667,1685,1688,1836,1839,1973,1976,1979,2037,2040,2154],[11,1391,1392],{"id":1392},"解决问题",[15,1394,1395],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[11,1397,1398],{"id":1398},"查阅资料",[167,1400,1402],{"className":169,"code":1401,"language":171,"meta":172,"style":172},"# 启动容器并挂载当前目录到 `\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",[36,1403,1404,1409,1447,1451,1456,1483,1499,1513,1518,1531,1553,1561,1565,1570,1580,1590,1594,1602,1606],{"__ignoreMap":172},[176,1405,1406],{"class":178,"line":179},[176,1407,1408],{"class":247},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[176,1410,1411,1414,1417,1420,1423,1426,1429,1432,1435,1438,1441,1444],{"class":178,"line":244},[176,1412,1413],{"class":182},"docker",[176,1415,1416],{"class":186}," run",[176,1418,1419],{"class":294}," -it",[176,1421,1422],{"class":294}," --rm",[176,1424,1425],{"class":294}," -v",[176,1427,1428],{"class":186}," \"",[176,1430,1431],{"class":228},"$PWD",[176,1433,1434],{"class":186},"\":\u002Fapp",[176,1436,1437],{"class":294}," -w",[176,1439,1440],{"class":186}," \u002Fapp",[176,1442,1443],{"class":186}," ubuntu:20.04",[176,1445,1446],{"class":186}," bash\n",[176,1448,1449],{"class":178,"line":251},[176,1450,263],{"emptyLinePlaceholder":262},[176,1452,1453],{"class":178,"line":259},[176,1454,1455],{"class":247},"# 在容器内执行以下操作：\n",[176,1457,1458,1461,1464,1467,1469,1471,1474,1477,1480],{"class":178,"line":266},[176,1459,1460],{"class":182},"apt-get",[176,1462,1463],{"class":186}," update",[176,1465,1466],{"class":228}," && ",[176,1468,1460],{"class":182},[176,1470,187],{"class":186},[176,1472,1473],{"class":294}," -y",[176,1475,1476],{"class":186}," musl-tools",[176,1478,1479],{"class":186}," golang-go",[176,1481,1482],{"class":247},"  # 安装 musl 和 Go\n",[176,1484,1485,1488,1491,1493,1496],{"class":178,"line":284},[176,1486,1487],{"class":224},"export",[176,1489,1490],{"class":228}," CC",[176,1492,326],{"class":224},[176,1494,1495],{"class":228},"musl-gcc  ",[176,1497,1498],{"class":247},"# 指定 musl 编译器\n",[176,1500,1501,1503,1506,1508,1510],{"class":178,"line":298},[176,1502,1487],{"class":224},[176,1504,1505],{"class":228}," CGO_ENABLED",[176,1507,326],{"class":224},[176,1509,1257],{"class":294},[176,1511,1512],{"class":247},"  # 启用 CGO\n",[176,1514,1515],{"class":178,"line":304},[176,1516,1517],{"class":247},"# 设置代理\n",[176,1519,1520,1523,1526,1528],{"class":178,"line":309},[176,1521,1522],{"class":182},"go",[176,1524,1525],{"class":186}," env",[176,1527,1437],{"class":294},[176,1529,1530],{"class":186}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[176,1532,1533,1535,1538,1541,1544,1547,1550],{"class":178,"line":350},[176,1534,1522],{"class":182},[176,1536,1537],{"class":186}," build",[176,1539,1540],{"class":294}," -ldflags",[176,1542,1543],{"class":186}," '-linkmode external -extldflags \"-static\"'",[176,1545,1546],{"class":294}," -o",[176,1548,1549],{"class":186}," myapp",[176,1551,1552],{"class":247},"  # 静态编译\n",[176,1554,1555,1558],{"class":178,"line":362},[176,1556,1557],{"class":294},"exit",[176,1559,1560],{"class":247},"  # 退出容器\n",[176,1562,1563],{"class":178,"line":383},[176,1564,263],{"emptyLinePlaceholder":262},[176,1566,1567],{"class":178,"line":389},[176,1568,1569],{"class":247},"# 验证结果（在宿主机执行）\n",[176,1571,1572,1575,1577],{"class":178,"line":395},[176,1573,1574],{"class":182},"file",[176,1576,1549],{"class":186},[176,1578,1579],{"class":247},"  # 应显示 \"statically linked\"\n",[176,1581,1582,1585,1587],{"class":178,"line":420},[176,1583,1584],{"class":182},"ldd",[176,1586,1549],{"class":186},[176,1588,1589],{"class":247},"   # 应显示 \"not a dynamic executable\"\n",[176,1591,1592],{"class":178,"line":426},[176,1593,263],{"emptyLinePlaceholder":262},[176,1595,1596,1599],{"class":178,"line":750},[176,1597,1598],{"class":182},"wget",[176,1600,1601],{"class":186}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[176,1603,1604],{"class":178,"line":761},[176,1605,263],{"emptyLinePlaceholder":262},[176,1607,1608,1611,1614,1617],{"class":178,"line":772},[176,1609,1610],{"class":182},"ENV",[176,1612,1613],{"class":186}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[176,1615,1616],{"class":228},"$PATH",[176,1618,1619],{"class":186},"\"\n",[162,1621,1622],{"id":1622},"遇到问题",[19,1624,1625,1650],{},[22,1626,1627,1630,1631,1634,1637,1638,1641,1643,1644,1646],{},[36,1628,1629],{},"go mod tidy"," 时报错 ",[36,1632,1633],{},"tls: failed to verify certificate: x509: certificate",[1635,1636],"br",{},"解决：",[36,1639,1640],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[1635,1642],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[1635,1645],{},[338,1647],{"alt":1648,"src":1649},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[22,1651,1652,1653,1656,1657,1659,1660,1663],{},"直接安装 ",[36,1654,1655],{},"golang-go"," 其版本过低",[1635,1658],{},"解决: ",[36,1661,1662],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[11,1665,1666],{"id":1666},"最终方案",[19,1668,1669,1676,1679],{},[22,1670,1671,1672,1675],{},"下载 ",[36,1673,1674],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[22,1677,1678],{},"编写 Dockerfile",[22,1680,1681,1682],{},"执行 ",[36,1683,1684],{},"docker build -t go-build:v1 .",[15,1686,1687],{},"Dockerfile 内容：",[167,1689,1693],{"className":1690,"code":1691,"language":1692,"meta":172,"style":172},"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",[36,1694,1695,1703,1707,1718,1722,1730,1734,1744,1748,1756,1760,1765,1772,1776,1783,1788,1795,1799,1807,1811,1818,1822],{"__ignoreMap":172},[176,1696,1697,1700],{"class":178,"line":179},[176,1698,1699],{"class":224},"FROM",[176,1701,1702],{"class":228}," ubuntu:20.04\n",[176,1704,1705],{"class":178,"line":244},[176,1706,263],{"emptyLinePlaceholder":262},[176,1708,1709,1712,1715],{"class":178,"line":251},[176,1710,1711],{"class":224},"LABEL",[176,1713,1714],{"class":228}," org.opencontainers.image.authors=",[176,1716,1717],{"class":186},"\"sutong\"\n",[176,1719,1720],{"class":178,"line":259},[176,1721,263],{"emptyLinePlaceholder":262},[176,1723,1724,1727],{"class":178,"line":266},[176,1725,1726],{"class":224},"COPY",[176,1728,1729],{"class":228}," go\u002F \u002Fgo\u002F\n",[176,1731,1732],{"class":178,"line":284},[176,1733,263],{"emptyLinePlaceholder":262},[176,1735,1736,1738,1741],{"class":178,"line":298},[176,1737,1610],{"class":224},[176,1739,1740],{"class":228}," PATH=",[176,1742,1743],{"class":186},"\"\u002Fgo\u002Fbin:$PATH\"\n",[176,1745,1746],{"class":178,"line":304},[176,1747,263],{"emptyLinePlaceholder":262},[176,1749,1750,1753],{"class":178,"line":309},[176,1751,1752],{"class":224},"RUN",[176,1754,1755],{"class":228}," apt-get update && apt-get install -y musl-tools\n",[176,1757,1758],{"class":178,"line":350},[176,1759,263],{"emptyLinePlaceholder":262},[176,1761,1762],{"class":178,"line":362},[176,1763,1764],{"class":247},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[176,1766,1767,1769],{"class":178,"line":383},[176,1768,1752],{"class":224},[176,1770,1771],{"class":228}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[176,1773,1774],{"class":178,"line":389},[176,1775,1498],{"class":247},[176,1777,1778,1780],{"class":178,"line":395},[176,1779,1610],{"class":224},[176,1781,1782],{"class":228}," CC=musl-gcc\n",[176,1784,1785],{"class":178,"line":420},[176,1786,1787],{"class":247},"# 启用 CGO\n",[176,1789,1790,1792],{"class":178,"line":426},[176,1791,1610],{"class":224},[176,1793,1794],{"class":228}," CGO_ENABLED=1\n",[176,1796,1797],{"class":178,"line":750},[176,1798,263],{"emptyLinePlaceholder":262},[176,1800,1801,1804],{"class":178,"line":761},[176,1802,1803],{"class":224},"VOLUME",[176,1805,1806],{"class":228}," \u002Fdata\n",[176,1808,1809],{"class":178,"line":772},[176,1810,263],{"emptyLinePlaceholder":262},[176,1812,1813,1816],{"class":178,"line":778},[176,1814,1815],{"class":224},"WORKDIR",[176,1817,1806],{"class":228},[176,1819,1820],{"class":178,"line":789},[176,1821,263],{"emptyLinePlaceholder":262},[176,1823,1824,1827,1830,1833],{"class":178,"line":807},[176,1825,1826],{"class":224},"CMD",[176,1828,1829],{"class":228}," [",[176,1831,1832],{"class":186},"\"\u002Fbin\u002Fbash\"",[176,1834,1835],{"class":228},"]\n",[162,1837,1838],{"id":1838},"构建",[167,1840,1842],{"className":169,"code":1841,"language":171,"meta":172,"style":172},"# 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",[36,1843,1844,1849,1853,1858,1884,1888,1893,1927,1932,1936,1968],{"__ignoreMap":172},[176,1845,1846],{"class":178,"line":179},[176,1847,1848],{"class":247},"# docker build -t go-build:v1 .\n",[176,1850,1851],{"class":178,"line":244},[176,1852,263],{"emptyLinePlaceholder":262},[176,1854,1855],{"class":178,"line":251},[176,1856,1857],{"class":247},"# 直接执行编译 编译后删除\n",[176,1859,1860,1862,1864,1866,1868,1871,1874,1877,1879,1881],{"class":178,"line":259},[176,1861,1413],{"class":182},[176,1863,1416],{"class":186},[176,1865,1422],{"class":294},[176,1867,1425],{"class":294},[176,1869,1870],{"class":186}," .:\u002Fdata",[176,1872,1873],{"class":186}," go-build:v1",[176,1875,1876],{"class":186}," go",[176,1878,1537],{"class":186},[176,1880,1546],{"class":294},[176,1882,1883],{"class":186}," app_musl\n",[176,1885,1886],{"class":178,"line":266},[176,1887,263],{"emptyLinePlaceholder":262},[176,1889,1890],{"class":178,"line":284},[176,1891,1892],{"class":247},"# 构建编译环境\n",[176,1894,1895,1897,1899,1901,1904,1906,1908,1911,1914,1917,1920,1922,1924],{"class":178,"line":298},[176,1896,1413],{"class":182},[176,1898,1416],{"class":186},[176,1900,1419],{"class":294},[176,1902,1903],{"class":294}," -d",[176,1905,1425],{"class":294},[176,1907,1870],{"class":186},[176,1909,1910],{"class":294}," --name",[176,1912,1913],{"class":224}," \u003C",[176,1915,1916],{"class":186},"容器名",[176,1918,1919],{"class":228},"称",[176,1921,1301],{"class":224},[176,1923,1873],{"class":186},[176,1925,1926],{"class":186}," \u002Fbin\u002Fbash\n",[176,1928,1929],{"class":178,"line":304},[176,1930,1931],{"class":247},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[176,1933,1934],{"class":178,"line":309},[176,1935,263],{"emptyLinePlaceholder":262},[176,1937,1938,1940,1943,1945,1947,1949,1951,1953,1955,1957,1959,1961,1964,1966],{"class":178,"line":350},[176,1939,1413],{"class":182},[176,1941,1942],{"class":186}," exec",[176,1944,1419],{"class":294},[176,1946,1913],{"class":224},[176,1948,1916],{"class":186},[176,1950,1919],{"class":228},[176,1952,1301],{"class":224},[176,1954,1876],{"class":186},[176,1956,1537],{"class":186},[176,1958,1546],{"class":294},[176,1960,1913],{"class":224},[176,1962,1963],{"class":186},"myap",[176,1965,15],{"class":228},[176,1967,712],{"class":224},[176,1969,1970],{"class":178,"line":362},[176,1971,1972],{"class":247},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[162,1974,1975],{"id":1975},"额外注意",[15,1977,1978],{},"若没科学上网需要设置代理：",[167,1980,1982],{"className":169,"code":1981,"language":171,"meta":172,"style":172},"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",[36,1983,1984,2002,2012,2028],{"__ignoreMap":172},[176,1985,1986,1988,1990,1992,1994,1996,1998,2000],{"class":178,"line":179},[176,1987,1413],{"class":182},[176,1989,1942],{"class":186},[176,1991,1419],{"class":294},[176,1993,1913],{"class":224},[176,1995,1916],{"class":186},[176,1997,1919],{"class":228},[176,1999,1301],{"class":224},[176,2001,1926],{"class":186},[176,2003,2004,2006,2008,2010],{"class":178,"line":244},[176,2005,1522],{"class":182},[176,2007,1525],{"class":186},[176,2009,1437],{"class":294},[176,2011,1530],{"class":186},[176,2013,2014,2016,2018,2020,2022,2024,2026],{"class":178,"line":251},[176,2015,1522],{"class":182},[176,2017,1537],{"class":186},[176,2019,1540],{"class":294},[176,2021,1543],{"class":186},[176,2023,1546],{"class":294},[176,2025,1549],{"class":186},[176,2027,1552],{"class":247},[176,2029,2030,2032,2034],{"class":178,"line":259},[176,2031,1522],{"class":182},[176,2033,1537],{"class":186},[176,2035,2036],{"class":247}," # musl 库编译\n",[162,2038,2039],{"id":2039},"运行环境示例",[167,2041,2043],{"className":1690,"code":2042,"language":1692,"meta":172,"style":172},"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",[36,2044,2045,2052,2056,2064,2068,2075,2079,2086,2093,2097,2104,2108,2115,2119,2140,2144,2149],{"__ignoreMap":172},[176,2046,2047,2049],{"class":178,"line":179},[176,2048,1699],{"class":224},[176,2050,2051],{"class":228}," alpine:latest\n",[176,2053,2054],{"class":178,"line":244},[176,2055,263],{"emptyLinePlaceholder":262},[176,2057,2058,2060,2062],{"class":178,"line":251},[176,2059,1711],{"class":224},[176,2061,1714],{"class":228},[176,2063,1717],{"class":186},[176,2065,2066],{"class":178,"line":259},[176,2067,263],{"emptyLinePlaceholder":262},[176,2069,2070,2072],{"class":178,"line":266},[176,2071,1726],{"class":224},[176,2073,2074],{"class":228}," glog_musl \u002Fapp\u002F\n",[176,2076,2077],{"class":178,"line":284},[176,2078,263],{"emptyLinePlaceholder":262},[176,2080,2081,2083],{"class":178,"line":298},[176,2082,1726],{"class":224},[176,2084,2085],{"class":228}," conf.ini \u002Fdata\u002F\n",[176,2087,2088,2090],{"class":178,"line":304},[176,2089,1726],{"class":224},[176,2091,2092],{"class":228}," ipname.ini \u002Fdata\u002F\n",[176,2094,2095],{"class":178,"line":309},[176,2096,263],{"emptyLinePlaceholder":262},[176,2098,2099,2101],{"class":178,"line":350},[176,2100,1815],{"class":224},[176,2102,2103],{"class":228}," \u002Fapp\n",[176,2105,2106],{"class":178,"line":362},[176,2107,263],{"emptyLinePlaceholder":262},[176,2109,2110,2112],{"class":178,"line":383},[176,2111,1752],{"class":224},[176,2113,2114],{"class":228}," chmod 777 \u002Fapp\u002Fglog_musl\n",[176,2116,2117],{"class":178,"line":389},[176,2118,263],{"emptyLinePlaceholder":262},[176,2120,2121,2123,2125,2128,2130,2133,2135,2138],{"class":178,"line":395},[176,2122,1826],{"class":224},[176,2124,1829],{"class":228},[176,2126,2127],{"class":186},"\".\u002Fglog_musl\"",[176,2129,612],{"class":228},[176,2131,2132],{"class":186},"\"logsweb\"",[176,2134,612],{"class":228},[176,2136,2137],{"class":186},"\"\u002Fdata\u002Fconf.ini\"",[176,2139,1835],{"class":228},[176,2141,2142],{"class":178,"line":420},[176,2143,263],{"emptyLinePlaceholder":262},[176,2145,2146],{"class":178,"line":426},[176,2147,2148],{"class":247},"# docker build -t glog:v1 .\n",[176,2150,2151],{"class":178,"line":750},[176,2152,2153],{"class":247},"# docker run -p 6801:6801 glog:v1\n",[1346,2155,2156],{},"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":172,"searchDepth":244,"depth":251,"links":2158},[2159,2160,2163],{"id":1392,"depth":244,"text":1392},{"id":1398,"depth":244,"text":1398,"children":2161},[2162],{"id":1622,"depth":251,"text":1622},{"id":1666,"depth":244,"text":1666,"children":2164},[2165,2166,2167],{"id":1838,"depth":251,"text":1838},{"id":1975,"depth":251,"text":1975},{"id":2039,"depth":251,"text":2039},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","进阶","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":1387,"description":2168},"Docker 实践笔记","posts\u002Fdocker-musl-build\u002Findex",[2178,2179,2180],"Docker","Go","运维","YLtdOOc-gMVDSNjE-ntKXz-7hff-VtvMB2dB5RXNi1o",{"id":2183,"title":2184,"author":6,"body":2185,"description":2584,"difficulty":1367,"draft":1368,"extension":1369,"image":2585,"meta":2586,"navigation":262,"path":2587,"pinned":1368,"published":2588,"seo":2589,"series":2175,"seriesOrder":244,"stem":2590,"tags":2591,"updated":2588,"__hash__":2593},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":8,"value":2186,"toc":2574},[2187,2190,2193,2196,2200,2203,2220,2224,2257,2261,2268,2316,2319,2532,2535,2538,2568,2571],[11,2188,2189],{"id":2189},"前言",[15,2191,2192],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[11,2194,2195],{"id":2195},"解决方案",[162,2197,2199],{"id":2198},"_1-安装用户管理工具","1. 安装用户管理工具",[15,2201,2202],{},"若镜像非自带用户管理：",[167,2204,2206],{"className":1690,"code":2205,"language":1692,"meta":172,"style":172},"# 权限管理\nRUN apk add --no-cache shadow\n",[36,2207,2208,2213],{"__ignoreMap":172},[176,2209,2210],{"class":178,"line":179},[176,2211,2212],{"class":247},"# 权限管理\n",[176,2214,2215,2217],{"class":178,"line":244},[176,2216,1752],{"class":224},[176,2218,2219],{"class":228}," apk add --no-cache shadow\n",[162,2221,2223],{"id":2222},"_2-创建用户并修改权限","2. 创建用户并修改权限",[167,2225,2227],{"className":1690,"code":2226,"language":1692,"meta":172,"style":172},"# 创建用户组和用户\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",[36,2228,2229,2234,2241,2245,2250],{"__ignoreMap":172},[176,2230,2231],{"class":178,"line":179},[176,2232,2233],{"class":247},"# 创建用户组和用户\n",[176,2235,2236,2238],{"class":178,"line":244},[176,2237,1752],{"class":224},[176,2239,2240],{"class":228}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[176,2242,2243],{"class":178,"line":251},[176,2244,263],{"emptyLinePlaceholder":262},[176,2246,2247],{"class":178,"line":259},[176,2248,2249],{"class":247},"# 修改目录权限\n",[176,2251,2252,2254],{"class":178,"line":266},[176,2253,1752],{"class":224},[176,2255,2256],{"class":228}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[162,2258,2260],{"id":2259},"_3-指定用户启动","3. 指定用户启动",[15,2262,2263,2264,2267],{},"执行 docker 时加 ",[36,2265,2266],{},"--user"," 参数：",[167,2269,2271],{"className":169,"code":2270,"language":171,"meta":172,"style":172},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[36,2272,2273],{"__ignoreMap":172},[176,2274,2275,2277,2279,2281,2284,2286,2289,2292,2294,2296,2299,2302,2304,2306,2308,2311,2314],{"class":178,"line":179},[176,2276,1413],{"class":182},[176,2278,1416],{"class":186},[176,2280,1419],{"class":294},[176,2282,2283],{"class":294}," --user",[176,2285,1913],{"class":224},[176,2287,2288],{"class":186},"app_use",[176,2290,2291],{"class":228},"r",[176,2293,1301],{"class":224},[176,2295,1913],{"class":224},[176,2297,2298],{"class":186},"app_imag",[176,2300,2301],{"class":228},"e",[176,2303,1301],{"class":224},[176,2305,318],{"class":186},[176,2307,443],{"class":224},[176,2309,2310],{"class":186},"image_versio",[176,2312,2313],{"class":228},"n",[176,2315,712],{"class":224},[11,2317,2318],{"id":2318},"完整示例",[167,2320,2322],{"className":1690,"code":2321,"language":1692,"meta":172,"style":172},"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",[36,2323,2324,2330,2334,2339,2352,2356,2363,2367,2373,2378,2385,2390,2397,2401,2408,2412,2416,2423,2427,2434,2438,2445,2449,2462,2468,2472,2477,2484,2491,2496,2502,2511],{"__ignoreMap":172},[176,2325,2326,2328],{"class":178,"line":179},[176,2327,1699],{"class":224},[176,2329,2051],{"class":228},[176,2331,2332],{"class":178,"line":244},[176,2333,263],{"emptyLinePlaceholder":262},[176,2335,2336],{"class":178,"line":251},[176,2337,2338],{"class":247},"# 国内源\n",[176,2340,2341,2343,2346,2349],{"class":178,"line":259},[176,2342,1752],{"class":224},[176,2344,2345],{"class":228}," sed -i ",[176,2347,2348],{"class":186},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[176,2350,2351],{"class":228}," \u002Fetc\u002Fapk\u002Frepositories\n",[176,2353,2354],{"class":178,"line":266},[176,2355,263],{"emptyLinePlaceholder":262},[176,2357,2358,2360],{"class":178,"line":284},[176,2359,1752],{"class":224},[176,2361,2362],{"class":228}," apk update --no-cache\n",[176,2364,2365],{"class":178,"line":298},[176,2366,2212],{"class":247},[176,2368,2369,2371],{"class":178,"line":304},[176,2370,1752],{"class":224},[176,2372,2219],{"class":228},[176,2374,2375],{"class":178,"line":309},[176,2376,2377],{"class":247},"# 调试\n",[176,2379,2380,2382],{"class":178,"line":350},[176,2381,1752],{"class":224},[176,2383,2384],{"class":228}," apk add --no-cache bash\n",[176,2386,2387],{"class":178,"line":362},[176,2388,2389],{"class":247},"# 设置时区\n",[176,2391,2392,2394],{"class":178,"line":383},[176,2393,1752],{"class":224},[176,2395,2396],{"class":228}," apk add --no-cache tzdata\n",[176,2398,2399],{"class":178,"line":389},[176,2400,263],{"emptyLinePlaceholder":262},[176,2402,2403,2405],{"class":178,"line":395},[176,2404,1610],{"class":224},[176,2406,2407],{"class":228}," TZ=Asia\u002FShanghai\n",[176,2409,2410],{"class":178,"line":420},[176,2411,263],{"emptyLinePlaceholder":262},[176,2413,2414],{"class":178,"line":426},[176,2415,2233],{"class":247},[176,2417,2418,2420],{"class":178,"line":750},[176,2419,1752],{"class":224},[176,2421,2422],{"class":228}," addgroup -S glog && adduser -S -G glog glog\n",[176,2424,2425],{"class":178,"line":761},[176,2426,263],{"emptyLinePlaceholder":262},[176,2428,2429,2431],{"class":178,"line":772},[176,2430,1726],{"class":224},[176,2432,2433],{"class":228}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[176,2435,2436],{"class":178,"line":778},[176,2437,263],{"emptyLinePlaceholder":262},[176,2439,2440,2442],{"class":178,"line":789},[176,2441,1752],{"class":224},[176,2443,2444],{"class":228}," mkdir \u002Fdata\n",[176,2446,2447],{"class":178,"line":807},[176,2448,263],{"emptyLinePlaceholder":262},[176,2450,2451,2453,2456,2459],{"class":178,"line":813},[176,2452,1803],{"class":224},[176,2454,2455],{"class":228}," [ ",[176,2457,2458],{"class":186},"\"\u002Fdata\"",[176,2460,2461],{"class":228}," ]\n",[176,2463,2464,2466],{"class":178,"line":823},[176,2465,1815],{"class":224},[176,2467,1806],{"class":228},[176,2469,2470],{"class":178,"line":833},[176,2471,263],{"emptyLinePlaceholder":262},[176,2473,2474],{"class":178,"line":839},[176,2475,2476],{"class":247},"# 修改权限\n",[176,2478,2479,2481],{"class":178,"line":845},[176,2480,1752],{"class":224},[176,2482,2483],{"class":228}," chown -R glog:glog \u002Fdata\n",[176,2485,2486,2488],{"class":178,"line":851},[176,2487,1752],{"class":224},[176,2489,2490],{"class":228}," chown -R glog:glog \u002Fapp\n",[176,2492,2494],{"class":178,"line":2493},29,[176,2495,263],{"emptyLinePlaceholder":262},[176,2497,2499],{"class":178,"line":2498},30,[176,2500,2501],{"class":247},"# 指定用户启动\n",[176,2503,2505,2508],{"class":178,"line":2504},31,[176,2506,2507],{"class":224},"USER",[176,2509,2510],{"class":228}," glog\n",[176,2512,2514,2516,2518,2521,2523,2525,2527,2530],{"class":178,"line":2513},32,[176,2515,1826],{"class":224},[176,2517,2455],{"class":228},[176,2519,2520],{"class":186},"\"\u002Fapp\u002Fglog_static_musl_1\"",[176,2522,612],{"class":228},[176,2524,2132],{"class":186},[176,2526,612],{"class":228},[176,2528,2529],{"class":186},"\".\u002Fconf.ini\"",[176,2531,2461],{"class":228},[11,2533,2534],{"id":2534},"总结",[15,2536,2537],{},"关键步骤：",[19,2539,2540,2546,2556,2562],{},[22,2541,2542,2545],{},[36,2543,2544],{},"apk add --no-cache shadow"," - 安装用户管理工具",[22,2547,2548,2551,2552,2555],{},[36,2549,2550],{},"addgroup"," + ",[36,2553,2554],{},"adduser"," - 创建用户",[22,2557,2558,2561],{},[36,2559,2560],{},"chown -R"," - 修改目录权限",[22,2563,2564,2567],{},[36,2565,2566],{},"USER \u003Cusername>"," - 指定运行用户",[15,2569,2570],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[1346,2572,2573],{},"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":172,"searchDepth":244,"depth":251,"links":2575},[2576,2577,2582,2583],{"id":2189,"depth":244,"text":2189},{"id":2195,"depth":244,"text":2195,"children":2578},[2579,2580,2581],{"id":2198,"depth":251,"text":2199},{"id":2222,"depth":251,"text":2223},{"id":2259,"depth":251,"text":2260},{"id":2318,"depth":244,"text":2318},{"id":2534,"depth":244,"text":2534},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":2184,"description":2584},"posts\u002Fdocker-user-start\u002Findex",[2178,2180,2592],"Linux","b4Uyw5ms87xr7NhpIkavHnNbT20oaEPtXz8k1w6UPj0",{"id":2595,"title":2596,"author":6,"body":2597,"description":2806,"difficulty":6,"draft":1368,"extension":1369,"image":2807,"meta":2808,"navigation":262,"path":2809,"pinned":1368,"published":2810,"seo":2811,"series":6,"seriesOrder":6,"stem":2812,"tags":2813,"updated":6,"__hash__":2815},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":8,"value":2598,"toc":2796},[2599,2603,2606,2612,2615,2619,2626,2677,2681,2687,2695,2699,2702,2705,2774,2776,2787,2793],[11,2600,2602],{"id":2601},"什么是-sveltekit","什么是 SvelteKit？",[15,2604,2605],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[15,2607,2608],{},[338,2609],{"alt":2610,"src":2611},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[11,2613,2614],{"id":2614},"核心特点",[162,2616,2618],{"id":2617},"_1-编译时框架","1. 编译时框架",[15,2620,2621,2622,2625],{},"Svelte 和 React\u002FVue 最大的区别在于：",[25,2623,2624],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[167,2627,2631],{"className":2628,"code":2629,"language":2630,"meta":172,"style":172},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n","typescript",[36,2632,2633,2638,2651,2662,2673],{"__ignoreMap":172},[176,2634,2635],{"class":178,"line":179},[176,2636,2637],{"class":247},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[176,2639,2640,2643,2646,2648],{"class":178,"line":244},[176,2641,2642],{"class":224},"let",[176,2644,2645],{"class":228}," count ",[176,2647,326],{"class":224},[176,2649,2650],{"class":294}," 0\n",[176,2652,2653,2656,2659],{"class":178,"line":251},[176,2654,2655],{"class":224},"function",[176,2657,2658],{"class":182}," increment",[176,2660,2661],{"class":228},"() {\n",[176,2663,2664,2667,2670],{"class":178,"line":259},[176,2665,2666],{"class":228},"  count ",[176,2668,2669],{"class":224},"+=",[176,2671,2672],{"class":294}," 1\n",[176,2674,2675],{"class":178,"line":266},[176,2676,301],{"class":228},[162,2678,2680],{"id":2679},"_2-文件路由","2. 文件路由",[15,2682,2683,2686],{},[36,2684,2685],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[167,2688,2693],{"className":2689,"code":2691,"language":2692},[2690],"language-text","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","text",[36,2694,2691],{"__ignoreMap":172},[162,2696,2698],{"id":2697},"_3-全栈能力","3. 全栈能力",[15,2700,2701],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[11,2703,2704],{"id":2704},"和其他框架对比",[48,2706,2707,2723],{},[51,2708,2709],{},[54,2710,2711,2714,2717,2720],{},[57,2712,2713],{},"特性",[57,2715,2716],{},"SvelteKit",[57,2718,2719],{},"Next.js",[57,2721,2722],{},"Nuxt",[70,2724,2725,2738,2751,2763],{},[54,2726,2727,2730,2733,2736],{},[75,2728,2729],{},"学习曲线",[75,2731,2732],{},"低",[75,2734,2735],{},"中",[75,2737,2735],{},[54,2739,2740,2743,2746,2749],{},[75,2741,2742],{},"包体积",[75,2744,2745],{},"小",[75,2747,2748],{},"大",[75,2750,2735],{},[54,2752,2753,2756,2758,2761],{},[75,2754,2755],{},"运行时开销",[75,2757,84],{},[75,2759,2760],{},"有",[75,2762,2760],{},[54,2764,2765,2768,2770,2772],{},[75,2766,2767],{},"生态",[75,2769,2745],{},[75,2771,2748],{},[75,2773,2748],{},[11,2775,2534],{"id":2534},[15,2777,2778,2779,2782,2783,2786],{},"如果你追求",[25,2780,2781],{},"简洁的代码","和",[25,2784,2785],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[15,2788,2789],{},[338,2790],{"alt":2791,"src":2792},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[1346,2794,2795],{},"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":172,"searchDepth":244,"depth":251,"links":2797},[2798,2799,2804,2805],{"id":2601,"depth":244,"text":2602},{"id":2614,"depth":244,"text":2614,"children":2800},[2801,2802,2803],{"id":2617,"depth":251,"text":2618},{"id":2679,"depth":251,"text":2680},{"id":2697,"depth":251,"text":2698},{"id":2704,"depth":244,"text":2704},{"id":2534,"depth":244,"text":2534},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":2596,"description":2806},"posts\u002Fsveltekit-intro\u002Findex",[2716,2814],"前端框架","vsEGyunpCWR7DiJAdyV1SPDfa57xXLITBI-lKM8UeGQ",{"id":2817,"title":2818,"author":6,"body":2819,"description":3217,"difficulty":1367,"draft":1368,"extension":1369,"image":3218,"meta":3219,"navigation":262,"path":3220,"pinned":262,"published":3221,"seo":3222,"series":3223,"seriesOrder":179,"stem":3224,"tags":3225,"updated":3227,"__hash__":3228},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎使用 SuBlog",{"type":8,"value":2820,"toc":3208},[2821,2825,2832,2835,2838,2841,2915,2918,2921,2983,2986,2989,3052,3055,3120,3123,3126,3142,3149,3152,3183,3186,3197,3200,3205],[11,2822,2824],{"id":2823},"博客上线啦","博客上线啦！",[15,2826,2827,2828,2831],{},"Hello World！欢迎使用 ",[25,2829,2830],{},"SuBlog","，这是站点的第一篇示例文章。",[15,2833,2834],{},"经过一段时间的折腾，这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。",[11,2836,2837],{"id":2837},"技术栈",[15,2839,2840],{},"本站使用了以下技术：",[48,2842,2843,2853],{},[51,2844,2845],{},[54,2846,2847,2850],{},[57,2848,2849],{},"技术",[57,2851,2852],{},"用途",[70,2854,2855,2865,2875,2885,2895,2905],{},[54,2856,2857,2862],{},[75,2858,2859],{},[25,2860,2861],{},"Nuxt 3",[75,2863,2864],{},"Vue 3 全栈框架，提供 SSG、路由、API 等能力",[54,2866,2867,2872],{},[75,2868,2869],{},[25,2870,2871],{},"TypeScript",[75,2873,2874],{},"类型安全，提升开发体验",[54,2876,2877,2882],{},[75,2878,2879],{},[25,2880,2881],{},"Tailwind CSS v4",[75,2883,2884],{},"原子化 CSS，快速构建 UI",[54,2886,2887,2892],{},[75,2888,2889],{},[25,2890,2891],{},"@nuxt\u002Fcontent",[75,2893,2894],{},"Markdown 驱动的内容管理",[54,2896,2897,2902],{},[75,2898,2899],{},[25,2900,2901],{},"Shiki",[75,2903,2904],{},"代码语法高亮",[54,2906,2907,2912],{},[75,2908,2909],{},[25,2910,2911],{},"Giscus",[75,2913,2914],{},"基于 GitHub Discussions 的评论系统",[11,2916,2917],{"id":2917},"功能特性",[15,2919,2920],{},"目前已实现的功能：",[859,2922,2923,2929,2935,2941,2947,2953,2959,2965,2971,2977],{},[22,2924,2925,2928],{},[25,2926,2927],{},"博客系统"," - Markdown 文章，支持代码高亮、目录导航、搜索",[22,2930,2931,2934],{},[25,2932,2933],{},"暗色模式"," - 跟随系统或手动切换",[22,2936,2937,2940],{},[25,2938,2939],{},"文章搜索"," - 支持标题、描述、正文、标签多维度搜索",[22,2942,2943,2946],{},[25,2944,2945],{},"标签系统"," - 文章标签分类，高频标签快速筛选",[22,2948,2949,2952],{},[25,2950,2951],{},"友链页面"," - 展示友情链接，支持申请",[22,2954,2955,2958],{},[25,2956,2957],{},"赞助页面"," - 微信收款码展示",[22,2960,2961,2964],{},[25,2962,2963],{},"封面制作"," - 在线生成封面图片工具",[22,2966,2967,2970],{},[25,2968,2969],{},"网易云热评"," - 首页随机展示音乐评论",[22,2972,2973,2976],{},[25,2974,2975],{},"访客统计"," - 不蒜子计数器，带数字滚动动画",[22,2978,2979,2982],{},[25,2980,2981],{},"SEO 优化"," - 自动生成站点地图、Open Graph 标签",[11,2984,2985],{"id":2985},"代码示例",[15,2987,2988],{},"本站的配置非常集中，一个文件搞定全站定制：",[167,2990,2992],{"className":2628,"code":2991,"language":2630,"meta":172,"style":172},"\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",[36,2993,2994,2999,3013,3023,3033,3043,3048],{"__ignoreMap":172},[176,2995,2996],{"class":178,"line":179},[176,2997,2998],{"class":247},"\u002F\u002F config\u002Fsite.ts\n",[176,3000,3001,3003,3006,3009,3011],{"class":178,"line":244},[176,3002,1487],{"class":224},[176,3004,3005],{"class":224}," const",[176,3007,3008],{"class":294}," siteConfig",[176,3010,1223],{"class":224},[176,3012,281],{"class":228},[176,3014,3015,3018,3021],{"class":178,"line":251},[176,3016,3017],{"class":228},"  siteName: ",[176,3019,3020],{"class":186},"'我的博客'",[176,3022,417],{"class":228},[176,3024,3025,3028,3031],{"class":178,"line":259},[176,3026,3027],{"class":228},"  url: ",[176,3029,3030],{"class":186},"'https:\u002F\u002Fexample.com\u002F'",[176,3032,417],{"class":228},[176,3034,3035,3038,3041],{"class":178,"line":266},[176,3036,3037],{"class":228},"  avatar: ",[176,3039,3040],{"class":186},"'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100'",[176,3042,417],{"class":228},[176,3044,3045],{"class":178,"line":284},[176,3046,3047],{"class":247},"  \u002F\u002F ... 改这一个文件，全站生效\n",[176,3049,3050],{"class":178,"line":298},[176,3051,301],{"class":228},[15,3053,3054],{},"文章的 frontmatter 也很简洁：",[167,3056,3060],{"className":3057,"code":3058,"language":3059,"meta":172,"style":172},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 文章标题\npublished: 2026-05-31T08:00:00\ndescription: 文章简介\ntags: ['Nuxt', 'TypeScript']\n---\n","yaml",[36,3061,3062,3067,3078,3088,3098,3116],{"__ignoreMap":172},[176,3063,3064],{"class":178,"line":179},[176,3065,3066],{"class":182},"---\n",[176,3068,3069,3072,3075],{"class":178,"line":244},[176,3070,3071],{"class":684},"title",[176,3073,3074],{"class":228},": ",[176,3076,3077],{"class":186},"文章标题\n",[176,3079,3080,3083,3085],{"class":178,"line":251},[176,3081,3082],{"class":684},"published",[176,3084,3074],{"class":228},[176,3086,3087],{"class":294},"2026-05-31T08:00:00\n",[176,3089,3090,3093,3095],{"class":178,"line":259},[176,3091,3092],{"class":684},"description",[176,3094,3074],{"class":228},[176,3096,3097],{"class":186},"文章简介\n",[176,3099,3100,3103,3106,3109,3111,3114],{"class":178,"line":266},[176,3101,3102],{"class":684},"tags",[176,3104,3105],{"class":228},": [",[176,3107,3108],{"class":186},"'Nuxt'",[176,3110,612],{"class":228},[176,3112,3113],{"class":186},"'TypeScript'",[176,3115,1835],{"class":228},[176,3117,3118],{"class":178,"line":284},[176,3119,3066],{"class":182},[11,3121,3122],{"id":3122},"部署",[15,3124,3125],{},"本站使用 SSG 静态生成，构建后直接部署到静态托管即可：",[167,3127,3129],{"className":169,"code":3128,"language":171,"meta":172,"style":172},"npx nuxi generate\n",[36,3130,3131],{"__ignoreMap":172},[176,3132,3133,3136,3139],{"class":178,"line":179},[176,3134,3135],{"class":182},"npx",[176,3137,3138],{"class":186}," nuxi",[176,3140,3141],{"class":186}," generate\n",[15,3143,3144,3145,3148],{},"构建产物在 ",[36,3146,3147],{},".output\u002Fpublic"," 目录下，可以直接丢到 Nginx、Vercel、Netlify 等平台。",[11,3150,3151],{"id":3151},"后续计划",[859,3153,3156,3165,3171,3177],{"className":3154},[3155],"contains-task-list",[22,3157,3160,3164],{"className":3158},[3159],"task-list-item",[3161,3162],"input",{"disabled":262,"type":3163},"checkbox"," RSS 订阅",[22,3166,3168,3170],{"className":3167},[3159],[3161,3169],{"disabled":262,"type":3163}," 文章目录优化",[22,3172,3174,3176],{"className":3173},[3159],[3161,3175],{"disabled":262,"type":3163}," 更多小工具",[22,3178,3180,3182],{"className":3179},[3159],[3161,3181],{"disabled":262,"type":3163}," 持续输出内容",[11,3184,3185],{"id":3185},"致谢",[15,3187,3188,3189,3196],{},"感谢 ",[3190,3191,3195],"a",{"href":3192,"rel":3193},"https:\u002F\u002F2x.nz",[3194],"nofollow","二叉树树"," 的开源项目，本站的 UI 设计参考了他的项目。",[3198,3199],"hr",{},[15,3201,3202],{},[25,3203,3204],{},"感谢阅读！如果觉得不错，欢迎点个 Star 或者留个评论 (☞ﾟヮﾟ)☞",[1346,3206,3207],{},"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":172,"searchDepth":244,"depth":251,"links":3209},[3210,3211,3212,3213,3214,3215,3216],{"id":2823,"depth":244,"text":2824},{"id":2837,"depth":244,"text":2837},{"id":2917,"depth":244,"text":2917},{"id":2985,"depth":244,"text":2985},{"id":3122,"depth":244,"text":3122},{"id":3151,"depth":244,"text":3151},{"id":3185,"depth":244,"text":3185},"博客上线啦！这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。","\u002Fposts\u002Fhello-world\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fhello-world","2026-05-31T08:00:00",{"title":2818,"description":3217},"Nuxt 博客开发","posts\u002Fhello-world\u002Findex",[3226,2722],"博客","2026-05-31","f9MCboCBfQIUxT8aKv6aKGDFxysiG8ZG0YXr5X5LTFo",{"id":3230,"title":3231,"author":6,"body":3232,"description":4390,"difficulty":2169,"draft":1368,"extension":1369,"image":4391,"meta":4392,"navigation":262,"path":4393,"pinned":1368,"published":4394,"seo":4395,"series":3223,"seriesOrder":244,"stem":4396,"tags":4397,"updated":3227,"__hash__":4400},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",{"type":8,"value":3233,"toc":4380},[3234,3238,3241,3244,3248,3251,3261,3264,3271,3339,3346,3348,3355,3623,3633,3637,3640,3643,3668,3682,3685,3708,3714,3717,3720,3723,3728,3735,3957,3962,4006,4010,4013,4020,4023,4072,4078,4307,4317,4319,4371,4374,4377],[3235,3236,3237],"h1",{"id":3237},"背景",[15,3239,3240],{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[15,3242,3243],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[3235,3245,3247],{"id":3246},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[11,3249,3250],{"id":3250},"现象",[15,3252,3253,3254,3257,3258,910],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[36,3255,3256],{},"post.body.toc"," 上。直接用它渲染目录，发现",[25,3259,3260],{},"只有 h2 标题，h3 全部丢失",[11,3262,3263],{"id":3263},"排查",[15,3265,3266,3267,3270],{},"查了官方文档，发现 ",[36,3268,3269],{},"build.markdown.toc.depth"," 可以控制深度：",[167,3272,3274],{"className":2628,"code":3273,"language":2630,"meta":172,"style":172},"\u002F\u002F nuxt.config.ts\ncontent: {\n  build: {\n    markdown: {\n      toc: { depth: 3 }  \u002F\u002F 包含 h3\n    }\n  }\n}\n",[36,3275,3276,3281,3289,3296,3303,3325,3330,3335],{"__ignoreMap":172},[176,3277,3278],{"class":178,"line":179},[176,3279,3280],{"class":247},"\u002F\u002F nuxt.config.ts\n",[176,3282,3283,3286],{"class":178,"line":244},[176,3284,3285],{"class":182},"content",[176,3287,3288],{"class":228},": {\n",[176,3290,3291,3294],{"class":178,"line":251},[176,3292,3293],{"class":182},"  build",[176,3295,3288],{"class":228},[176,3297,3298,3301],{"class":178,"line":259},[176,3299,3300],{"class":182},"    markdown",[176,3302,3288],{"class":228},[176,3304,3305,3308,3311,3314,3316,3319,3322],{"class":178,"line":266},[176,3306,3307],{"class":182},"      toc",[176,3309,3310],{"class":228},": { ",[176,3312,3313],{"class":182},"depth",[176,3315,3074],{"class":228},[176,3317,3318],{"class":294},"3",[176,3320,3321],{"class":228}," }  ",[176,3323,3324],{"class":247},"\u002F\u002F 包含 h3\n",[176,3326,3327],{"class":178,"line":284},[176,3328,3329],{"class":228},"    }\n",[176,3331,3332],{"class":178,"line":298},[176,3333,3334],{"class":228},"  }\n",[176,3336,3337],{"class":178,"line":304},[176,3338,301],{"class":228},[15,3340,3341,3342,3345],{},"配置改了，清了缓存重启，",[25,3343,3344],{},"依然只有 h2","。这个配置似乎不起作用。",[11,3347,1666],{"id":1666},[15,3349,3350,3351,3354],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[36,3352,3353],{},"post.body"," 的 AST 里递归提取所有标题：",[167,3356,3358],{"className":2628,"code":3357,"language":2630,"meta":172,"style":172},"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",[36,3359,3360,3401,3419,3432,3437,3496,3524,3556,3580,3584,3608,3612,3619],{"__ignoreMap":172},[176,3361,3362,3364,3367,3369,3372,3374,3377,3379,3382,3384,3387,3389,3391,3394,3396,3398],{"class":178,"line":179},[176,3363,2655],{"class":224},[176,3365,3366],{"class":182}," extractHeadings",[176,3368,1237],{"class":228},[176,3370,3371],{"class":287},"node",[176,3373,318],{"class":224},[176,3375,3376],{"class":294}," any",[176,3378,612],{"class":228},[176,3380,3381],{"class":287},"list",[176,3383,318],{"class":224},[176,3385,3386],{"class":182}," Heading",[176,3388,323],{"class":228},[176,3390,326],{"class":224},[176,3392,3393],{"class":228}," [])",[176,3395,318],{"class":224},[176,3397,3386],{"class":182},[176,3399,3400],{"class":228},"[] {\n",[176,3402,3403,3406,3408,3411,3414,3416],{"class":178,"line":244},[176,3404,3405],{"class":224},"  if",[176,3407,1062],{"class":228},[176,3409,3410],{"class":224},"!",[176,3412,3413],{"class":228},"node) ",[176,3415,1272],{"class":224},[176,3417,3418],{"class":228}," list\n",[176,3420,3421,3423,3426,3429],{"class":178,"line":251},[176,3422,3405],{"class":224},[176,3424,3425],{"class":228}," (Array.",[176,3427,3428],{"class":182},"isArray",[176,3430,3431],{"class":228},"(node)) {\n",[176,3433,3434],{"class":178,"line":259},[176,3435,3436],{"class":247},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[176,3438,3439,3442,3444,3447,3450,3452,3455,3458,3461,3464,3467,3470,3474,3477,3480,3483,3485,3488,3491,3493],{"class":178,"line":266},[176,3440,3441],{"class":224},"    if",[176,3443,1062],{"class":228},[176,3445,3446],{"class":224},"typeof",[176,3448,3449],{"class":228}," node[",[176,3451,552],{"class":294},[176,3453,3454],{"class":228},"] ",[176,3456,3457],{"class":224},"===",[176,3459,3460],{"class":186}," 'string'",[176,3462,3463],{"class":224}," &&",[176,3465,3466],{"class":186}," \u002F",[176,3468,3469],{"class":224},"^",[176,3471,3473],{"class":3472},"sPKmS","h",[176,3475,3476],{"class":294},"[1-6]",[176,3478,3479],{"class":224},"$",[176,3481,3482],{"class":186},"\u002F",[176,3484,408],{"class":228},[176,3486,3487],{"class":182},"test",[176,3489,3490],{"class":228},"(node[",[176,3492,552],{"class":294},[176,3494,3495],{"class":228},"])) {\n",[176,3497,3498,3500,3503,3505,3508,3510,3512,3514,3517,3519,3521],{"class":178,"line":284},[176,3499,626],{"class":224},[176,3501,3502],{"class":294}," level",[176,3504,1223],{"class":224},[176,3506,3507],{"class":182}," Number",[176,3509,3490],{"class":228},[176,3511,552],{"class":294},[176,3513,1231],{"class":228},[176,3515,3516],{"class":182},"charAt",[176,3518,1237],{"class":228},[176,3520,1257],{"class":294},[176,3522,3523],{"class":228},"))\n",[176,3525,3526,3528,3531,3533,3536,3539,3542,3544,3547,3550,3553],{"class":178,"line":298},[176,3527,626],{"class":224},[176,3529,3530],{"class":294}," text",[176,3532,1223],{"class":224},[176,3534,3535],{"class":182}," extractText",[176,3537,3538],{"class":228},"(node.",[176,3540,3541],{"class":182},"slice",[176,3543,1237],{"class":228},[176,3545,3546],{"class":294},"2",[176,3548,3549],{"class":228},")).",[176,3551,3552],{"class":182},"trim",[176,3554,3555],{"class":228},"()\n",[176,3557,3558,3561,3564,3567,3569,3572,3574,3577],{"class":178,"line":304},[176,3559,3560],{"class":228},"      list.",[176,3562,3563],{"class":182},"push",[176,3565,3566],{"class":228},"({ id: node[",[176,3568,1257],{"class":294},[176,3570,3571],{"class":228},"]?.id ",[176,3573,374],{"class":224},[176,3575,3576],{"class":182}," slugify",[176,3578,3579],{"class":228},"(text), text, level })\n",[176,3581,3582],{"class":178,"line":309},[176,3583,3329],{"class":228},[176,3585,3586,3589,3591,3593,3596,3599,3602,3605],{"class":178,"line":350},[176,3587,3588],{"class":224},"    for",[176,3590,1062],{"class":228},[176,3592,312],{"class":224},[176,3594,3595],{"class":294}," child",[176,3597,3598],{"class":224}," of",[176,3600,3601],{"class":228}," node) ",[176,3603,3604],{"class":182},"extractHeadings",[176,3606,3607],{"class":228},"(child, list)\n",[176,3609,3610],{"class":178,"line":362},[176,3611,3334],{"class":228},[176,3613,3614,3617],{"class":178,"line":383},[176,3615,3616],{"class":224},"  return",[176,3618,3418],{"class":228},[176,3620,3621],{"class":178,"line":389},[176,3622,301],{"class":228},[15,3624,3625,3626,3628,3629,3632],{},"关键点：Nuxt Content v3 的 body 使用 ",[25,3627,8],{}," 格式，标题节点是数组 ",[36,3630,3631],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[3235,3634,3636],{"id":3635},"问题二图片路径解析错误","问题二：图片路径解析错误",[11,3638,3250],{"id":3639},"现象-1",[15,3641,3642],{},"markdown 里用相对路径引用图片：",[167,3644,3648],{"className":3645,"code":3646,"language":3647,"meta":172,"style":172},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[36,3649,3650],{"__ignoreMap":172},[176,3651,3652,3655,3659,3662,3666],{"class":178,"line":179},[176,3653,3654],{"class":228},"![",[176,3656,3658],{"class":3657},"smiIW","SSHFS挂载效果",[176,3660,3661],{"class":228},"](",[176,3663,3665],{"class":3664},"szbl0","img\u002Fsshfs.avif",[176,3667,1243],{"class":228},[15,3669,3670,3671,3674,3675,3678,3679,910],{},"HTML 输出的 ",[36,3672,3673],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[36,3676,3677],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[36,3680,3681],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[11,3683,3263],{"id":3684},"排查-1",[19,3686,3687,3694,3697,3703],{},[22,3688,3689,3690,3693],{},"检查了 ",[36,3691,3692],{},"\u003Cbase>"," 标签 —— 没有",[22,3695,3696],{},"检查了重定向 —— 没有",[22,3698,3699,3700,3702],{},"直接访问 ",[36,3701,3681],{}," —— 返回 200 OK",[22,3704,3699,3705,3707],{},[36,3706,3677],{}," —— 404",[15,3709,3710,3711,910],{},"服务器路由没问题，问题是",[25,3712,3713],{},"浏览器解析相对路径时基于了错误的基准 URL",[15,3715,3716],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[11,3718,1666],{"id":3719},"最终方案-1",[15,3721,3722],{},"两步解决：",[15,3724,3725],{},[25,3726,3727],{},"第一步：自定义 ProseImg 组件",[15,3729,3730,3731,3734],{},"创建 ",[36,3732,3733],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[167,3736,3740],{"className":3737,"code":3738,"language":3739,"meta":172,"style":172},"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",[36,3741,3742,3754,3768,3787,3803,3840,3874,3896,3901,3910,3914,3923,3949],{"__ignoreMap":172},[176,3743,3744,3746,3749,3752],{"class":178,"line":179},[176,3745,443],{"class":228},[176,3747,3748],{"class":684},"script",[176,3750,3751],{"class":182}," setup",[176,3753,712],{"class":228},[176,3755,3756,3758,3761,3763,3766],{"class":178,"line":244},[176,3757,312],{"class":224},[176,3759,3760],{"class":294}," route",[176,3762,1223],{"class":224},[176,3764,3765],{"class":182}," useRoute",[176,3767,3555],{"class":228},[176,3769,3770,3772,3775,3777,3780,3783,3785],{"class":178,"line":251},[176,3771,312],{"class":224},[176,3773,3774],{"class":294}," resolvedSrc",[176,3776,1223],{"class":224},[176,3778,3779],{"class":182}," computed",[176,3781,3782],{"class":228},"(() ",[176,3784,344],{"class":224},[176,3786,281],{"class":228},[176,3788,3789,3791,3793,3795,3798,3800],{"class":178,"line":259},[176,3790,3405],{"class":224},[176,3792,1062],{"class":228},[176,3794,3410],{"class":224},[176,3796,3797],{"class":228},"props.src) ",[176,3799,1272],{"class":224},[176,3801,3802],{"class":186}," ''\n",[176,3804,3805,3807,3810,3813,3815,3818,3820,3822,3825,3827,3829,3832,3835,3837],{"class":178,"line":266},[176,3806,3405],{"class":224},[176,3808,3809],{"class":228}," (props.src.",[176,3811,3812],{"class":182},"startsWith",[176,3814,1237],{"class":228},[176,3816,3817],{"class":186},"'\u002F'",[176,3819,341],{"class":228},[176,3821,374],{"class":224},[176,3823,3824],{"class":228}," props.src.",[176,3826,3812],{"class":182},[176,3828,1237],{"class":228},[176,3830,3831],{"class":186},"'http'",[176,3833,3834],{"class":228},")) ",[176,3836,1272],{"class":224},[176,3838,3839],{"class":228}," props.src\n",[176,3841,3842,3844,3847,3849,3852,3855,3857,3859,3863,3865,3867,3869,3872],{"class":178,"line":284},[176,3843,1115],{"class":224},[176,3845,3846],{"class":294}," base",[176,3848,1223],{"class":224},[176,3850,3851],{"class":228}," route.path.",[176,3853,3854],{"class":182},"replace",[176,3856,1237],{"class":228},[176,3858,3482],{"class":186},[176,3860,3862],{"class":3861},"sCnZR","\\\u002F",[176,3864,3479],{"class":224},[176,3866,3482],{"class":186},[176,3868,612],{"class":228},[176,3870,3871],{"class":186},"''",[176,3873,1243],{"class":228},[176,3875,3876,3878,3881,3884,3887,3889,3891,3893],{"class":178,"line":298},[176,3877,3616],{"class":224},[176,3879,3880],{"class":186}," `${",[176,3882,3883],{"class":228},"base",[176,3885,3886],{"class":186},"}\u002F${",[176,3888,609],{"class":228},[176,3890,408],{"class":186},[176,3892,204],{"class":228},[176,3894,3895],{"class":186},"}`\n",[176,3897,3898],{"class":178,"line":304},[176,3899,3900],{"class":228},"})\n",[176,3902,3903,3906,3908],{"class":178,"line":309},[176,3904,3905],{"class":228},"\u003C\u002F",[176,3907,3748],{"class":684},[176,3909,712],{"class":228},[176,3911,3912],{"class":178,"line":350},[176,3913,263],{"emptyLinePlaceholder":262},[176,3915,3916,3918,3921],{"class":178,"line":362},[176,3917,443],{"class":228},[176,3919,3920],{"class":684},"template",[176,3922,712],{"class":228},[176,3924,3925,3928,3930,3933,3935,3938,3941,3943,3946],{"class":178,"line":383},[176,3926,3927],{"class":228},"  \u003C",[176,3929,338],{"class":684},[176,3931,3932],{"class":182}," :src",[176,3934,326],{"class":228},[176,3936,3937],{"class":186},"\"resolvedSrc\"",[176,3939,3940],{"class":182}," :alt",[176,3942,326],{"class":228},[176,3944,3945],{"class":186},"\"alt\"",[176,3947,3948],{"class":228}," \u002F>\n",[176,3950,3951,3953,3955],{"class":178,"line":389},[176,3952,3905],{"class":228},[176,3954,3920],{"class":684},[176,3956,712],{"class":228},[15,3958,3959],{},[25,3960,3961],{},"第二步：启用 MDC prose 组件",[167,3963,3965],{"className":2628,"code":3964,"language":2630,"meta":172,"style":172},"\u002F\u002F nuxt.config.ts\nmdc: {\n  components: {\n    prose: true  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n  }\n}\n",[36,3966,3967,3971,3978,3985,3998,4002],{"__ignoreMap":172},[176,3968,3969],{"class":178,"line":179},[176,3970,3280],{"class":247},[176,3972,3973,3976],{"class":178,"line":244},[176,3974,3975],{"class":182},"mdc",[176,3977,3288],{"class":228},[176,3979,3980,3983],{"class":178,"line":251},[176,3981,3982],{"class":182},"  components",[176,3984,3288],{"class":228},[176,3986,3987,3990,3992,3995],{"class":178,"line":259},[176,3988,3989],{"class":182},"    prose",[176,3991,3074],{"class":228},[176,3993,3994],{"class":294},"true",[176,3996,3997],{"class":247},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[176,3999,4000],{"class":178,"line":266},[176,4001,3334],{"class":228},[176,4003,4004],{"class":178,"line":284},[176,4005,301],{"class":228},[3235,4007,4009],{"id":4008},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[11,4011,3250],{"id":4012},"现象-2",[15,4014,4015,4016,4019],{},"原站（SvelteKit）的图片放在 ",[36,4017,4018],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[11,4021,4022],{"id":4022},"方案对比",[48,4024,4025,4037],{},[51,4026,4027],{},[54,4028,4029,4031,4034],{},[57,4030,59],{},[57,4032,4033],{},"优点",[57,4035,4036],{},"缺点",[70,4038,4039,4050,4061],{},[54,4040,4041,4044,4047],{},[75,4042,4043],{},"复制到 public\u002F",[75,4045,4046],{},"简单",[75,4048,4049],{},"文件重复，需同步",[54,4051,4052,4055,4058],{},[75,4053,4054],{},"Vite 插件拦截",[75,4056,4057],{},"原站方案",[75,4059,4060],{},"Nuxt 不直接支持",[54,4062,4063,4066,4069],{},[75,4064,4065],{},"Server Route",[75,4067,4068],{},"不复制文件",[75,4070,4071],{},"需要手动写路由",[15,4073,4074,4075,4077],{},"最终选择 ",[25,4076,4065],{},"，和原站的 Vite 插件思路一致：",[167,4079,4081],{"className":2628,"code":4080,"language":2630,"meta":172,"style":172},"\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",[36,4082,4083,4088,4114,4129,4193,4207,4211,4233,4259,4277,4296,4303],{"__ignoreMap":172},[176,4084,4085],{"class":178,"line":179},[176,4086,4087],{"class":247},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[176,4089,4090,4092,4095,4098,4100,4103,4105,4108,4110,4112],{"class":178,"line":244},[176,4091,1487],{"class":224},[176,4093,4094],{"class":224}," default",[176,4096,4097],{"class":182}," defineEventHandler",[176,4099,1237],{"class":228},[176,4101,4102],{"class":224},"async",[176,4104,1062],{"class":228},[176,4106,4107],{"class":287},"event",[176,4109,341],{"class":228},[176,4111,344],{"class":224},[176,4113,281],{"class":228},[176,4115,4116,4118,4121,4123,4126],{"class":178,"line":251},[176,4117,1115],{"class":224},[176,4119,4120],{"class":294}," url",[176,4122,1223],{"class":224},[176,4124,4125],{"class":182}," getRequestURL",[176,4127,4128],{"class":228},"(event)\n",[176,4130,4131,4133,4136,4138,4141,4144,4146,4148,4150,4152,4155,4157,4159,4162,4164,4167,4170,4173,4175,4177,4179,4181,4183,4185,4187,4189,4191],{"class":178,"line":259},[176,4132,1115],{"class":224},[176,4134,4135],{"class":294}," match",[176,4137,1223],{"class":224},[176,4139,4140],{"class":228}," url.pathname.",[176,4142,4143],{"class":182},"match",[176,4145,1237],{"class":228},[176,4147,3482],{"class":186},[176,4149,3469],{"class":224},[176,4151,3862],{"class":3861},[176,4153,4154],{"class":3472},"posts",[176,4156,3862],{"class":3861},[176,4158,1237],{"class":3472},[176,4160,4161],{"class":294},"[",[176,4163,3469],{"class":224},[176,4165,4166],{"class":294},"\u002F]",[176,4168,4169],{"class":224},"+",[176,4171,4172],{"class":3472},")",[176,4174,3862],{"class":3861},[176,4176,338],{"class":3472},[176,4178,3862],{"class":3861},[176,4180,1237],{"class":3472},[176,4182,408],{"class":294},[176,4184,4169],{"class":224},[176,4186,4172],{"class":3472},[176,4188,3479],{"class":224},[176,4190,3482],{"class":186},[176,4192,1243],{"class":228},[176,4194,4195,4197,4199,4201,4204],{"class":178,"line":266},[176,4196,3405],{"class":224},[176,4198,1062],{"class":228},[176,4200,3410],{"class":224},[176,4202,4203],{"class":228},"match) ",[176,4205,4206],{"class":224},"return\n",[176,4208,4209],{"class":178,"line":284},[176,4210,263],{"emptyLinePlaceholder":262},[176,4212,4213,4215,4218,4221,4223,4226,4228,4230],{"class":178,"line":298},[176,4214,1115],{"class":224},[176,4216,4217],{"class":228}," [, ",[176,4219,4220],{"class":294},"slug",[176,4222,612],{"class":228},[176,4224,4225],{"class":294},"filename",[176,4227,3454],{"class":228},[176,4229,326],{"class":224},[176,4231,4232],{"class":228}," match\n",[176,4234,4235,4237,4240,4242,4245,4247,4250,4253,4256],{"class":178,"line":304},[176,4236,1115],{"class":224},[176,4238,4239],{"class":294}," filePath",[176,4241,1223],{"class":224},[176,4243,4244],{"class":182}," resolve",[176,4246,1237],{"class":228},[176,4248,4249],{"class":186},"'content\u002Fposts'",[176,4251,4252],{"class":228},", slug, ",[176,4254,4255],{"class":186},"'img'",[176,4257,4258],{"class":228},", filename)\n",[176,4260,4261,4263,4266,4268,4271,4274],{"class":178,"line":309},[176,4262,1115],{"class":224},[176,4264,4265],{"class":294}," data",[176,4267,1223],{"class":224},[176,4269,4270],{"class":224}," await",[176,4272,4273],{"class":182}," readFile",[176,4275,4276],{"class":228},"(filePath)\n",[176,4278,4279,4282,4285,4288,4290,4293],{"class":178,"line":350},[176,4280,4281],{"class":182},"  setResponseHeader",[176,4283,4284],{"class":228},"(event, ",[176,4286,4287],{"class":186},"'content-type'",[176,4289,612],{"class":228},[176,4291,4292],{"class":294},"MIME",[176,4294,4295],{"class":228},"[ext])\n",[176,4297,4298,4300],{"class":178,"line":362},[176,4299,3616],{"class":224},[176,4301,4302],{"class":228}," data\n",[176,4304,4305],{"class":178,"line":383},[176,4306,3900],{"class":228},[15,4308,4309,4310,4312,4313,4316],{},"请求 ",[36,4311,3681],{}," → 读取 ",[36,4314,4315],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[3235,4318,2534],{"id":2534},[48,4320,4321,4333],{},[51,4322,4323],{},[54,4324,4325,4328,4331],{},[57,4326,4327],{},"问题",[57,4329,4330],{},"根因",[57,4332,59],{},[70,4334,4335,4349,4360],{},[54,4336,4337,4340,4346],{},[75,4338,4339],{},"TOC 只有 h2",[75,4341,4342,4345],{},[36,4343,4344],{},"toc.depth"," 配置不生效",[75,4347,4348],{},"从 body AST 手动提取",[54,4350,4351,4354,4357],{},[75,4352,4353],{},"图片路径错误",[75,4355,4356],{},"SPA 路由导致相对路径解析基准错误",[75,4358,4359],{},"ProseImg 组件 + mdc.prose 配置",[54,4361,4362,4365,4368],{},[75,4363,4364],{},"content 图片 404",[75,4366,4367],{},"content 目录不暴露给浏览器",[75,4369,4370],{},"Server Route 直接读取返回",[15,4372,4373],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[15,4375,4376],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[1346,4378,4379],{},"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":172,"searchDepth":244,"depth":251,"links":4381},[4382,4383,4384,4385,4386,4387,4388,4389],{"id":3250,"depth":244,"text":3250},{"id":3263,"depth":244,"text":3263},{"id":1666,"depth":244,"text":1666},{"id":3639,"depth":244,"text":3250},{"id":3684,"depth":244,"text":3263},{"id":3719,"depth":244,"text":1666},{"id":4012,"depth":244,"text":3250},{"id":4022,"depth":244,"text":4022},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":3231,"description":4390},"posts\u002Fnuxt-content-lessons\u002Findex",[2722,4398,4399],"踩坑","Nuxt Content","vbSt6ND1z7xt7L8FxgZ8Qe3EdxV9InsRerN3E23GC7s",{"id":4402,"title":4403,"author":2830,"body":4404,"description":4667,"difficulty":2169,"draft":1368,"extension":1369,"image":4668,"meta":4669,"navigation":262,"path":4670,"pinned":1368,"published":4671,"seo":4672,"series":3223,"seriesOrder":251,"stem":4673,"tags":4674,"updated":3227,"__hash__":4677},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题",{"type":8,"value":4405,"toc":4660},[4406,4409,4412,4427,4430,4433,4436,4477,4488,4494,4516,4523,4527,4530,4533,4535,4538,4561,4572,4628,4633,4646,4648,4654,4657],[11,4407,4408],{"id":4408},"问题描述",[15,4410,4411],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[859,4413,4414,4424],{},[22,4415,4416,4417,4420,4421],{},"首次通过客户端导航（点击 ",[36,4418,4419],{},"\u003CNuxtLink>","）跳转到某个页面时，",[25,4422,4423],{},"所有链接文字都会出现下划线",[22,4425,4426],{},"手动刷新页面后，下划线消失，样式恢复正常",[15,4428,4429],{},"这个问题在开发模式和生产构建后都会出现。",[11,4431,4432],{"id":4432},"原因分析",[15,4434,4435],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[167,4437,4441],{"className":4438,"code":4439,"language":4440,"meta":172,"style":172},"language-css shiki shiki-themes github-light github-dark","a {\n  color: inherit;\n  text-decoration: inherit;\n}\n","css",[36,4442,4443,4449,4462,4473],{"__ignoreMap":172},[176,4444,4445,4447],{"class":178,"line":179},[176,4446,3190],{"class":684},[176,4448,281],{"class":228},[176,4450,4451,4454,4456,4459],{"class":178,"line":244},[176,4452,4453],{"class":294},"  color",[176,4455,3074],{"class":228},[176,4457,4458],{"class":294},"inherit",[176,4460,4461],{"class":228},";\n",[176,4463,4464,4467,4469,4471],{"class":178,"line":251},[176,4465,4466],{"class":294},"  text-decoration",[176,4468,3074],{"class":228},[176,4470,4458],{"class":294},[176,4472,4461],{"class":228},[176,4474,4475],{"class":178,"line":259},[176,4476,301],{"class":228},[15,4478,4479,4480,4483,4484,4487],{},"正常情况下，这会让链接继承父元素的 ",[36,4481,4482],{},"text-decoration","（通常是 ",[36,4485,4486],{},"none","），从而去掉浏览器默认的下划线。",[15,4489,4490,4491,1032],{},"问题出在 ",[25,4492,4493],{},"Nuxt 的客户端导航机制",[19,4495,4496,4502,4505,4510,4513],{},[22,4497,4498,4499,4501],{},"用户点击 ",[36,4500,4419],{},"，Vue Router 拦截导航",[22,4503,4504],{},"新页面组件异步加载并渲染",[22,4506,4507],{},[25,4508,4509],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[22,4511,4512],{},"此时浏览器使用默认样式（链接带下划线）",[22,4514,4515],{},"CSS 加载完成后，下划线消失",[15,4517,4518,4519,4522],{},"这就是典型的 ",[25,4520,4521],{},"FOUC（Flash of Unstyled Content）"," 问题。",[11,4524,4526],{"id":4525},"为什么刷新就正常了","为什么刷新就正常了？",[15,4528,4529],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[15,4531,4532],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[11,4534,2195],{"id":2195},[15,4536,4537],{},"尝试了多种方案：",[859,4539,4540,4546,4552,4558],{},[22,4541,4542,4545],{},[36,4543,4544],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[22,4547,4548,4551],{},[36,4549,4550],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[22,4553,4554,4557],{},[36,4555,4556],{},"features.inlineStyles: true"," — 无效",[22,4559,4560],{},"页面过渡动画 — 只是掩盖，不能根治",[15,4562,4563,4564,4567,4568,4571],{},"最终有效的方案是在 ",[36,4565,4566],{},"app.vue"," 中用 ",[36,4569,4570],{},"!important"," 强制覆盖：",[167,4573,4575],{"className":3737,"code":4574,"language":3739,"meta":172,"style":172},"\u003Cstyle>\na:not(.prose a) {\n  text-decoration-line: none !important;\n}\n\u003C\u002Fstyle>\n",[36,4576,4577,4585,4602,4616,4620],{"__ignoreMap":172},[176,4578,4579,4581,4583],{"class":178,"line":179},[176,4580,443],{"class":228},[176,4582,1346],{"class":684},[176,4584,712],{"class":228},[176,4586,4587,4589,4592,4594,4597,4599],{"class":178,"line":244},[176,4588,3190],{"class":684},[176,4590,4591],{"class":182},":not",[176,4593,1237],{"class":228},[176,4595,4596],{"class":182},".prose",[176,4598,1220],{"class":684},[176,4600,4601],{"class":228},") {\n",[176,4603,4604,4607,4609,4611,4614],{"class":178,"line":251},[176,4605,4606],{"class":294},"  text-decoration-line",[176,4608,3074],{"class":228},[176,4610,4486],{"class":294},[176,4612,4613],{"class":224}," !important",[176,4615,4461],{"class":228},[176,4617,4618],{"class":178,"line":259},[176,4619,301],{"class":228},[176,4621,4622,4624,4626],{"class":178,"line":266},[176,4623,3905],{"class":228},[176,4625,1346],{"class":684},[176,4627,712],{"class":228},[15,4629,4630],{},[25,4631,4632],{},"关键点：",[859,4634,4635,4640],{},[22,4636,4637,4639],{},[36,4638,4570],{}," 是为了对抗 Tailwind CSS layers 的优先级",[22,4641,4642,4645],{},[36,4643,4644],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[11,4647,2534],{"id":2534},[15,4649,4650,4651,4653],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[36,4652,4570],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[15,4655,4656],{},"如果你有更好的方案，欢迎在项目仓库的 Issues 中提出！",[1346,4658,4659],{},"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":172,"searchDepth":244,"depth":251,"links":4661},[4662,4663,4664,4665,4666],{"id":4408,"depth":244,"text":4408},{"id":4432,"depth":244,"text":4432},{"id":4525,"depth":244,"text":4526},{"id":2195,"depth":244,"text":2195},{"id":2534,"depth":244,"text":2534},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":4403,"description":4667},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[2722,4675,4676,4398],"Tailwind CSS","CSS","WMLpTEsIoJqufwEfXyIiBgUYxcPY2XkvOwqAD4hbe6g",{"id":4679,"title":4680,"author":6,"body":4681,"description":6723,"difficulty":2169,"draft":1368,"extension":1369,"image":6724,"meta":6725,"navigation":262,"path":6726,"pinned":1368,"published":6727,"seo":6728,"series":6729,"seriesOrder":179,"stem":6730,"tags":6731,"updated":6736,"__hash__":6737},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",{"type":8,"value":4682,"toc":6704},[4683,4687,4690,4693,4697,4704,4710,4713,4716,4736,4739,4743,4767,4771,4870,4883,4887,5050,5053,5270,5274,5285,5384,5391,5395,5668,5671,5752,5756,5759,5761,5772,5776,5782,6147,6334,6337,6393,6396,6549,6552,6555,6626,6628,6692,6701],[11,4684,4686],{"id":4685},"什么是-mcp","什么是 MCP",[15,4688,4689],{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[15,4691,4692],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[11,4694,4696],{"id":4695},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[15,4698,4699,4700,4703],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[36,4701,4702],{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[15,4705,4706,4707],{},"核心思路：",[25,4708,4709],{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[11,4711,4712],{"id":4712},"需求分析",[15,4714,4715],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[19,4717,4718,4724,4730],{},[22,4719,4720,4723],{},[25,4721,4722],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[22,4725,4726,4729],{},[25,4727,4728],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[22,4731,4732,4735],{},[25,4733,4734],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[11,4737,4738],{"id":4738},"实现步骤",[162,4740,4742],{"id":4741},"_1-安装依赖","1. 安装依赖",[167,4744,4746],{"className":169,"code":4745,"language":171,"meta":172,"style":172},"go get github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\ngo get github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[36,4747,4748,4758],{"__ignoreMap":172},[176,4749,4750,4752,4755],{"class":178,"line":179},[176,4751,1522],{"class":182},[176,4753,4754],{"class":186}," get",[176,4756,4757],{"class":186}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[176,4759,4760,4762,4764],{"class":178,"line":244},[176,4761,1522],{"class":182},[176,4763,4754],{"class":186},[176,4765,4766],{"class":186}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[162,4768,4770],{"id":4769},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[167,4772,4775],{"className":4773,"code":4774,"language":1522,"meta":172,"style":172},"language-go shiki shiki-themes github-light github-dark","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",[36,4776,4777,4788,4792,4809,4820,4831,4842,4851,4866],{"__ignoreMap":172},[176,4778,4779,4781,4783,4786],{"class":178,"line":179},[176,4780,225],{"class":224},[176,4782,1428],{"class":186},[176,4784,4785],{"class":182},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[176,4787,1619],{"class":186},[176,4789,4790],{"class":178,"line":244},[176,4791,263],{"emptyLinePlaceholder":262},[176,4793,4794,4797,4800,4803,4806],{"class":178,"line":251},[176,4795,4796],{"class":228},"mcpServer ",[176,4798,4799],{"class":224},":=",[176,4801,4802],{"class":228}," server.",[176,4804,4805],{"class":182},"NewMCPServer",[176,4807,4808],{"class":228},"(\n",[176,4810,4811,4814,4817],{"class":178,"line":259},[176,4812,4813],{"class":186},"    \"my-app\"",[176,4815,4816],{"class":228},",    ",[176,4818,4819],{"class":247},"\u002F\u002F 服务器名称\n",[176,4821,4822,4825,4828],{"class":178,"line":266},[176,4823,4824],{"class":186},"    \"1.0.0\"",[176,4826,4827],{"class":228},",     ",[176,4829,4830],{"class":247},"\u002F\u002F 版本\n",[176,4832,4833,4836,4839],{"class":178,"line":284},[176,4834,4835],{"class":228},"    server.",[176,4837,4838],{"class":182},"WithLogging",[176,4840,4841],{"class":228},"(),\n",[176,4843,4844,4846,4849],{"class":178,"line":298},[176,4845,4835],{"class":228},[176,4847,4848],{"class":182},"WithRecovery",[176,4850,4841],{"class":228},[176,4852,4853,4855,4858,4860,4863],{"class":178,"line":304},[176,4854,4835],{"class":228},[176,4856,4857],{"class":182},"WithInstructions",[176,4859,1237],{"class":228},[176,4861,4862],{"class":186},"\"这是一个 MCP 服务器示例\"",[176,4864,4865],{"class":228},"),\n",[176,4867,4868],{"class":178,"line":309},[176,4869,1243],{"class":228},[15,4871,4872,4874,4875,4878,4879,4882],{},[36,4873,4805],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[36,4876,4877],{},"tools\u002Flist","）和工具调用（",[36,4880,4881],{},"tools\u002Fcall","）的协议逻辑。",[162,4884,4886],{"id":4885},"_3-注册工具","3. 注册工具",[167,4888,4890],{"className":4773,"code":4889,"language":1522,"meta":172,"style":172},"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",[36,4891,4892,4903,4907,4912,4932,4947,4951,4955,4960,5020,5025,5046],{"__ignoreMap":172},[176,4893,4894,4896,4898,4901],{"class":178,"line":179},[176,4895,225],{"class":224},[176,4897,1428],{"class":186},[176,4899,4900],{"class":182},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[176,4902,1619],{"class":186},[176,4904,4905],{"class":178,"line":244},[176,4906,263],{"emptyLinePlaceholder":262},[176,4908,4909],{"class":178,"line":251},[176,4910,4911],{"class":247},"\u002F\u002F 定义工具\n",[176,4913,4914,4917,4919,4922,4925,4927,4930],{"class":178,"line":259},[176,4915,4916],{"class":228},"listTool ",[176,4918,4799],{"class":224},[176,4920,4921],{"class":228}," mcp.",[176,4923,4924],{"class":182},"NewTool",[176,4926,1237],{"class":228},[176,4928,4929],{"class":186},"\"list_items\"",[176,4931,417],{"class":228},[176,4933,4934,4937,4940,4942,4945],{"class":178,"line":266},[176,4935,4936],{"class":228},"    mcp.",[176,4938,4939],{"class":182},"WithDescription",[176,4941,1237],{"class":228},[176,4943,4944],{"class":186},"\"列出所有项目\"",[176,4946,4865],{"class":228},[176,4948,4949],{"class":178,"line":284},[176,4950,1243],{"class":228},[176,4952,4953],{"class":178,"line":298},[176,4954,263],{"emptyLinePlaceholder":262},[176,4956,4957],{"class":178,"line":304},[176,4958,4959],{"class":247},"\u002F\u002F 注册处理器\n",[176,4961,4962,4965,4968,4971,4974,4976,4979,4982,4984,4986,4988,4991,4994,4996,4999,5002,5005,5008,5010,5013,5015,5018],{"class":178,"line":309},[176,4963,4964],{"class":228},"mcpServer.",[176,4966,4967],{"class":182},"AddTool",[176,4969,4970],{"class":228},"(listTool, ",[176,4972,4973],{"class":224},"func",[176,4975,1237],{"class":228},[176,4977,4978],{"class":287},"ctx",[176,4980,4981],{"class":182}," context",[176,4983,408],{"class":228},[176,4985,1054],{"class":182},[176,4987,612],{"class":228},[176,4989,4990],{"class":287},"req",[176,4992,4993],{"class":182}," mcp",[176,4995,408],{"class":228},[176,4997,4998],{"class":182},"CallToolRequest",[176,5000,5001],{"class":228},") (",[176,5003,5004],{"class":224},"*",[176,5006,5007],{"class":182},"mcp",[176,5009,408],{"class":228},[176,5011,5012],{"class":182},"CallToolResult",[176,5014,612],{"class":228},[176,5016,5017],{"class":224},"error",[176,5019,4601],{"class":228},[176,5021,5022],{"class":178,"line":350},[176,5023,5024],{"class":247},"    \u002F\u002F 业务逻辑\n",[176,5026,5027,5030,5032,5035,5037,5040,5043],{"class":178,"line":362},[176,5028,5029],{"class":224},"    return",[176,5031,4921],{"class":228},[176,5033,5034],{"class":182},"NewToolResultText",[176,5036,1237],{"class":228},[176,5038,5039],{"class":186},"\"返回结果\"",[176,5041,5042],{"class":228},"), ",[176,5044,5045],{"class":294},"nil\n",[176,5047,5048],{"class":178,"line":383},[176,5049,3900],{"class":228},[15,5051,5052],{},"带参数的工具：",[167,5054,5056],{"className":4773,"code":5055,"language":1522,"meta":172,"style":172},"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",[36,5057,5058,5076,5089,5103,5113,5127,5132,5136,5140,5187,5206,5221,5240,5244,5249,5266],{"__ignoreMap":172},[176,5059,5060,5063,5065,5067,5069,5071,5074],{"class":178,"line":179},[176,5061,5062],{"class":228},"getTool ",[176,5064,4799],{"class":224},[176,5066,4921],{"class":228},[176,5068,4924],{"class":182},[176,5070,1237],{"class":228},[176,5072,5073],{"class":186},"\"get_item\"",[176,5075,417],{"class":228},[176,5077,5078,5080,5082,5084,5087],{"class":178,"line":244},[176,5079,4936],{"class":228},[176,5081,4939],{"class":182},[176,5083,1237],{"class":228},[176,5085,5086],{"class":186},"\"获取指定项目的详情\"",[176,5088,4865],{"class":228},[176,5090,5091,5093,5096,5098,5101],{"class":178,"line":251},[176,5092,4936],{"class":228},[176,5094,5095],{"class":182},"WithString",[176,5097,1237],{"class":228},[176,5099,5100],{"class":186},"\"item_id\"",[176,5102,417],{"class":228},[176,5104,5105,5108,5111],{"class":178,"line":259},[176,5106,5107],{"class":228},"        mcp.",[176,5109,5110],{"class":182},"Required",[176,5112,4841],{"class":228},[176,5114,5115,5117,5120,5122,5125],{"class":178,"line":266},[176,5116,5107],{"class":228},[176,5118,5119],{"class":182},"Description",[176,5121,1237],{"class":228},[176,5123,5124],{"class":186},"\"项目 ID\"",[176,5126,4865],{"class":228},[176,5128,5129],{"class":178,"line":284},[176,5130,5131],{"class":228},"    ),\n",[176,5133,5134],{"class":178,"line":298},[176,5135,1243],{"class":228},[176,5137,5138],{"class":178,"line":304},[176,5139,263],{"emptyLinePlaceholder":262},[176,5141,5142,5144,5146,5149,5151,5153,5155,5157,5159,5161,5163,5165,5167,5169,5171,5173,5175,5177,5179,5181,5183,5185],{"class":178,"line":309},[176,5143,4964],{"class":228},[176,5145,4967],{"class":182},[176,5147,5148],{"class":228},"(getTool, ",[176,5150,4973],{"class":224},[176,5152,1237],{"class":228},[176,5154,4978],{"class":287},[176,5156,4981],{"class":182},[176,5158,408],{"class":228},[176,5160,1054],{"class":182},[176,5162,612],{"class":228},[176,5164,4990],{"class":287},[176,5166,4993],{"class":182},[176,5168,408],{"class":228},[176,5170,4998],{"class":182},[176,5172,5001],{"class":228},[176,5174,5004],{"class":224},[176,5176,5007],{"class":182},[176,5178,408],{"class":228},[176,5180,5012],{"class":182},[176,5182,612],{"class":228},[176,5184,5017],{"class":224},[176,5186,4601],{"class":228},[176,5188,5189,5192,5194,5197,5200,5202,5204],{"class":178,"line":350},[176,5190,5191],{"class":228},"    itemID, err ",[176,5193,4799],{"class":224},[176,5195,5196],{"class":228}," req.",[176,5198,5199],{"class":182},"RequireString",[176,5201,1237],{"class":228},[176,5203,5100],{"class":186},[176,5205,1243],{"class":228},[176,5207,5208,5210,5213,5216,5219],{"class":178,"line":362},[176,5209,3441],{"class":224},[176,5211,5212],{"class":228}," err ",[176,5214,5215],{"class":224},"!=",[176,5217,5218],{"class":294}," nil",[176,5220,281],{"class":228},[176,5222,5223,5226,5228,5231,5233,5236,5238],{"class":178,"line":383},[176,5224,5225],{"class":224},"        return",[176,5227,4921],{"class":228},[176,5229,5230],{"class":182},"NewToolResultError",[176,5232,1237],{"class":228},[176,5234,5235],{"class":186},"\"缺少 item_id 参数\"",[176,5237,5042],{"class":228},[176,5239,5045],{"class":294},[176,5241,5242],{"class":178,"line":389},[176,5243,3329],{"class":228},[176,5245,5246],{"class":178,"line":395},[176,5247,5248],{"class":247},"    \u002F\u002F 查询并返回\n",[176,5250,5251,5253,5255,5257,5259,5262,5264],{"class":178,"line":420},[176,5252,5029],{"class":224},[176,5254,4921],{"class":228},[176,5256,5034],{"class":182},[176,5258,1237],{"class":228},[176,5260,5261],{"class":186},"\"项目详情...\"",[176,5263,5042],{"class":228},[176,5265,5045],{"class":294},[176,5267,5268],{"class":178,"line":426},[176,5269,3900],{"class":228},[162,5271,5273],{"id":5272},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[15,5275,5276,5277,5280,5281,5284],{},"关键一步：用 ",[36,5278,5279],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[36,5282,5283],{},"gin.WrapH"," 适配 Gin。",[167,5286,5288],{"className":4773,"code":5287,"language":1522,"meta":172,"style":172},"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",[36,5289,5290,5300,5304,5319,5333,5350,5354,5358,5363],{"__ignoreMap":172},[176,5291,5292,5294,5296,5298],{"class":178,"line":179},[176,5293,225],{"class":224},[176,5295,1428],{"class":186},[176,5297,4785],{"class":182},[176,5299,1619],{"class":186},[176,5301,5302],{"class":178,"line":244},[176,5303,263],{"emptyLinePlaceholder":262},[176,5305,5306,5309,5311,5313,5316],{"class":178,"line":251},[176,5307,5308],{"class":228},"mcpHTTPServer ",[176,5310,4799],{"class":224},[176,5312,4802],{"class":228},[176,5314,5315],{"class":182},"NewStreamableHTTPServer",[176,5317,5318],{"class":228},"(mcpServer,\n",[176,5320,5321,5323,5326,5328,5331],{"class":178,"line":259},[176,5322,4835],{"class":228},[176,5324,5325],{"class":182},"WithEndpointPath",[176,5327,1237],{"class":228},[176,5329,5330],{"class":186},"\"\u002Fmcp\"",[176,5332,4865],{"class":228},[176,5334,5335,5337,5340,5342,5345,5347],{"class":178,"line":266},[176,5336,4835],{"class":228},[176,5338,5339],{"class":182},"WithHeartbeatInterval",[176,5341,1237],{"class":228},[176,5343,5344],{"class":294},"30",[176,5346,5004],{"class":224},[176,5348,5349],{"class":228},"time.Second),\n",[176,5351,5352],{"class":178,"line":284},[176,5353,1243],{"class":228},[176,5355,5356],{"class":178,"line":298},[176,5357,263],{"emptyLinePlaceholder":262},[176,5359,5360],{"class":178,"line":304},[176,5361,5362],{"class":247},"\u002F\u002F 挂载到 Gin\n",[176,5364,5365,5368,5371,5373,5375,5378,5381],{"class":178,"line":309},[176,5366,5367],{"class":228},"r.",[176,5369,5370],{"class":182},"Any",[176,5372,1237],{"class":228},[176,5374,5330],{"class":186},[176,5376,5377],{"class":228},", gin.",[176,5379,5380],{"class":182},"WrapH",[176,5382,5383],{"class":228},"(mcpHTTPServer))\n",[15,5385,5386,5387,5390],{},"就这样，",[36,5388,5389],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[162,5392,5394],{"id":5393},"_5-完整的服务启动代码","5. 完整的服务启动代码",[167,5396,5398],{"className":4773,"code":5397,"language":1522,"meta":172,"style":172},"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",[36,5399,5400,5409,5424,5428,5433,5456,5465,5473,5478,5482,5487,5495,5499,5504,5517,5529,5543,5547,5565,5569,5574,5607,5641,5646,5650,5664],{"__ignoreMap":172},[176,5401,5402,5404,5407],{"class":178,"line":179},[176,5403,4973],{"class":224},[176,5405,5406],{"class":182}," main",[176,5408,2661],{"class":228},[176,5410,5411,5414,5416,5419,5422],{"class":178,"line":244},[176,5412,5413],{"class":228},"    r ",[176,5415,4799],{"class":224},[176,5417,5418],{"class":228}," gin.",[176,5420,5421],{"class":182},"Default",[176,5423,3555],{"class":228},[176,5425,5426],{"class":178,"line":251},[176,5427,263],{"emptyLinePlaceholder":262},[176,5429,5430],{"class":178,"line":259},[176,5431,5432],{"class":247},"    \u002F\u002F 创建 MCP 服务器\n",[176,5434,5435,5438,5440,5442,5444,5446,5449,5451,5454],{"class":178,"line":266},[176,5436,5437],{"class":228},"    mcpServer ",[176,5439,4799],{"class":224},[176,5441,4802],{"class":228},[176,5443,4805],{"class":182},[176,5445,1237],{"class":228},[176,5447,5448],{"class":186},"\"my-app\"",[176,5450,612],{"class":228},[176,5452,5453],{"class":186},"\"1.0.0\"",[176,5455,417],{"class":228},[176,5457,5458,5461,5463],{"class":178,"line":284},[176,5459,5460],{"class":228},"        server.",[176,5462,4838],{"class":182},[176,5464,4841],{"class":228},[176,5466,5467,5469,5471],{"class":178,"line":298},[176,5468,5460],{"class":228},[176,5470,4848],{"class":182},[176,5472,4841],{"class":228},[176,5474,5475],{"class":178,"line":304},[176,5476,5477],{"class":228},"    )\n",[176,5479,5480],{"class":178,"line":309},[176,5481,263],{"emptyLinePlaceholder":262},[176,5483,5484],{"class":178,"line":350},[176,5485,5486],{"class":247},"    \u002F\u002F 注册工具\n",[176,5488,5489,5492],{"class":178,"line":362},[176,5490,5491],{"class":182},"    registerTools",[176,5493,5494],{"class":228},"(mcpServer)\n",[176,5496,5497],{"class":178,"line":383},[176,5498,263],{"emptyLinePlaceholder":262},[176,5500,5501],{"class":178,"line":389},[176,5502,5503],{"class":247},"    \u002F\u002F 挂载到 Gin\n",[176,5505,5506,5509,5511,5513,5515],{"class":178,"line":395},[176,5507,5508],{"class":228},"    mcpHTTP ",[176,5510,4799],{"class":224},[176,5512,4802],{"class":228},[176,5514,5315],{"class":182},[176,5516,5318],{"class":228},[176,5518,5519,5521,5523,5525,5527],{"class":178,"line":420},[176,5520,5460],{"class":228},[176,5522,5325],{"class":182},[176,5524,1237],{"class":228},[176,5526,5330],{"class":186},[176,5528,4865],{"class":228},[176,5530,5531,5533,5535,5537,5539,5541],{"class":178,"line":426},[176,5532,5460],{"class":228},[176,5534,5339],{"class":182},[176,5536,1237],{"class":228},[176,5538,5344],{"class":294},[176,5540,5004],{"class":224},[176,5542,5349],{"class":228},[176,5544,5545],{"class":178,"line":750},[176,5546,5477],{"class":228},[176,5548,5549,5552,5554,5556,5558,5560,5562],{"class":178,"line":761},[176,5550,5551],{"class":228},"    r.",[176,5553,5370],{"class":182},[176,5555,1237],{"class":228},[176,5557,5330],{"class":186},[176,5559,5377],{"class":228},[176,5561,5380],{"class":182},[176,5563,5564],{"class":228},"(mcpHTTP))\n",[176,5566,5567],{"class":178,"line":772},[176,5568,263],{"emptyLinePlaceholder":262},[176,5570,5571],{"class":178,"line":778},[176,5572,5573],{"class":247},"    \u002F\u002F 其他业务路由\n",[176,5575,5576,5578,5581,5583,5586,5588,5590,5592,5595,5598,5601,5603,5605],{"class":178,"line":789},[176,5577,5551],{"class":228},[176,5579,5580],{"class":182},"GET",[176,5582,1237],{"class":228},[176,5584,5585],{"class":186},"\"\u002Fapi\u002Fhealth\"",[176,5587,612],{"class":228},[176,5589,4973],{"class":224},[176,5591,1237],{"class":228},[176,5593,5594],{"class":287},"c",[176,5596,5597],{"class":224}," *",[176,5599,5600],{"class":182},"gin",[176,5602,408],{"class":228},[176,5604,1054],{"class":182},[176,5606,4601],{"class":228},[176,5608,5609,5612,5615,5617,5620,5622,5624,5626,5629,5631,5634,5636,5639],{"class":178,"line":807},[176,5610,5611],{"class":228},"        c.",[176,5613,5614],{"class":182},"JSON",[176,5616,1237],{"class":228},[176,5618,5619],{"class":294},"200",[176,5621,612],{"class":228},[176,5623,5600],{"class":182},[176,5625,408],{"class":228},[176,5627,5628],{"class":182},"H",[176,5630,535],{"class":228},[176,5632,5633],{"class":186},"\"status\"",[176,5635,3074],{"class":228},[176,5637,5638],{"class":186},"\"ok\"",[176,5640,3900],{"class":228},[176,5642,5643],{"class":178,"line":813},[176,5644,5645],{"class":228},"    })\n",[176,5647,5648],{"class":178,"line":823},[176,5649,263],{"emptyLinePlaceholder":262},[176,5651,5652,5654,5657,5659,5662],{"class":178,"line":833},[176,5653,5551],{"class":228},[176,5655,5656],{"class":182},"Run",[176,5658,1237],{"class":228},[176,5660,5661],{"class":186},"\":8080\"",[176,5663,1243],{"class":228},[176,5665,5666],{"class":178,"line":839},[176,5667,301],{"class":228},[15,5669,5670],{},"此时启动服务，用 curl 测试：",[167,5672,5674],{"className":169,"code":5673,"language":171,"meta":172,"style":172},"# 发现工具列表\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",[36,5675,5676,5681,5698,5708,5716,5720,5725,5737,5745],{"__ignoreMap":172},[176,5677,5678],{"class":178,"line":179},[176,5679,5680],{"class":247},"# 发现工具列表\n",[176,5682,5683,5686,5689,5692,5695],{"class":178,"line":244},[176,5684,5685],{"class":182},"curl",[176,5687,5688],{"class":294}," -X",[176,5690,5691],{"class":186}," POST",[176,5693,5694],{"class":186}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[176,5696,5697],{"class":294}," \\\n",[176,5699,5700,5703,5706],{"class":178,"line":251},[176,5701,5702],{"class":294},"  -H",[176,5704,5705],{"class":186}," \"Content-Type: application\u002Fjson\"",[176,5707,5697],{"class":294},[176,5709,5710,5713],{"class":178,"line":259},[176,5711,5712],{"class":294},"  -d",[176,5714,5715],{"class":186}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[176,5717,5718],{"class":178,"line":266},[176,5719,263],{"emptyLinePlaceholder":262},[176,5721,5722],{"class":178,"line":284},[176,5723,5724],{"class":247},"# 调用工具\n",[176,5726,5727,5729,5731,5733,5735],{"class":178,"line":298},[176,5728,5685],{"class":182},[176,5730,5688],{"class":294},[176,5732,5691],{"class":186},[176,5734,5694],{"class":186},[176,5736,5697],{"class":294},[176,5738,5739,5741,5743],{"class":178,"line":304},[176,5740,5702],{"class":294},[176,5742,5705],{"class":186},[176,5744,5697],{"class":294},[176,5746,5747,5749],{"class":178,"line":309},[176,5748,5712],{"class":294},[176,5750,5751],{"class":186}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[11,5753,5755],{"id":5754},"拓展接入鉴权","拓展：接入鉴权",[15,5757,5758],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[162,5760,4327],{"id":4327},[15,5762,5763,5764,5767,5768,5771],{},"MCP 的工具处理器签名为 ",[36,5765,5766],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[36,5769,5770],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[162,5773,5775],{"id":5774},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[15,5777,5778,5779,910],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[36,5780,5781],{},"request.Context",[167,5783,5785],{"className":4773,"code":5784,"language":1522,"meta":172,"style":172},"\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",[36,5786,5787,5792,5820,5841,5861,5876,5909,5914,5919,5923,5948,5987,6016,6020,6024,6028,6048,6060,6089,6093,6097,6101,6116,6130,6139,6143],{"__ignoreMap":172},[176,5788,5789],{"class":178,"line":179},[176,5790,5791],{"class":247},"\u002F\u002F Gin 鉴权中间件\n",[176,5793,5794,5796,5799,5801,5804,5806,5809,5811,5813,5815,5818],{"class":178,"line":244},[176,5795,4973],{"class":224},[176,5797,5798],{"class":182}," AuthRequired",[176,5800,1237],{"class":228},[176,5802,5803],{"class":287},"authService",[176,5805,5597],{"class":224},[176,5807,5808],{"class":182},"AuthService",[176,5810,341],{"class":228},[176,5812,5600],{"class":182},[176,5814,408],{"class":228},[176,5816,5817],{"class":182},"HandlerFunc",[176,5819,281],{"class":228},[176,5821,5822,5824,5827,5829,5831,5833,5835,5837,5839],{"class":178,"line":251},[176,5823,5029],{"class":224},[176,5825,5826],{"class":224}," func",[176,5828,1237],{"class":228},[176,5830,5594],{"class":287},[176,5832,5597],{"class":224},[176,5834,5600],{"class":182},[176,5836,408],{"class":228},[176,5838,1054],{"class":182},[176,5840,4601],{"class":228},[176,5842,5843,5846,5848,5851,5854,5856,5859],{"class":178,"line":259},[176,5844,5845],{"class":228},"        authHeader ",[176,5847,4799],{"class":224},[176,5849,5850],{"class":228}," c.",[176,5852,5853],{"class":182},"GetHeader",[176,5855,1237],{"class":228},[176,5857,5858],{"class":186},"\"Authorization\"",[176,5860,1243],{"class":228},[176,5862,5863,5866,5869,5872,5874],{"class":178,"line":266},[176,5864,5865],{"class":224},"        if",[176,5867,5868],{"class":228}," authHeader ",[176,5870,5871],{"class":224},"==",[176,5873,745],{"class":186},[176,5875,281],{"class":228},[176,5877,5878,5881,5884,5886,5889,5891,5893,5895,5897,5899,5902,5904,5907],{"class":178,"line":284},[176,5879,5880],{"class":228},"            c.",[176,5882,5883],{"class":182},"AbortWithStatusJSON",[176,5885,1237],{"class":228},[176,5887,5888],{"class":294},"401",[176,5890,612],{"class":228},[176,5892,5600],{"class":182},[176,5894,408],{"class":228},[176,5896,5628],{"class":182},[176,5898,535],{"class":228},[176,5900,5901],{"class":186},"\"message\"",[176,5903,3074],{"class":228},[176,5905,5906],{"class":186},"\"缺少认证令牌\"",[176,5908,3900],{"class":228},[176,5910,5911],{"class":178,"line":298},[176,5912,5913],{"class":224},"            return\n",[176,5915,5916],{"class":178,"line":304},[176,5917,5918],{"class":228},"        }\n",[176,5920,5921],{"class":178,"line":309},[176,5922,263],{"emptyLinePlaceholder":262},[176,5924,5925,5928,5930,5933,5936,5939,5942,5944,5946],{"class":178,"line":350},[176,5926,5927],{"class":228},"        parts ",[176,5929,4799],{"class":224},[176,5931,5932],{"class":228}," strings.",[176,5934,5935],{"class":182},"SplitN",[176,5937,5938],{"class":228},"(authHeader, ",[176,5940,5941],{"class":186},"\" \"",[176,5943,612],{"class":228},[176,5945,3546],{"class":294},[176,5947,1243],{"class":228},[176,5949,5950,5952,5955,5958,5960,5962,5965,5968,5971,5974,5977,5979,5982,5985],{"class":178,"line":362},[176,5951,5865],{"class":224},[176,5953,5954],{"class":182}," len",[176,5956,5957],{"class":228},"(parts) ",[176,5959,5215],{"class":224},[176,5961,487],{"class":294},[176,5963,5964],{"class":224}," ||",[176,5966,5967],{"class":224}," !",[176,5969,5970],{"class":228},"strings.",[176,5972,5973],{"class":182},"EqualFold",[176,5975,5976],{"class":228},"(parts[",[176,5978,552],{"class":294},[176,5980,5981],{"class":228},"], ",[176,5983,5984],{"class":186},"\"Bearer\"",[176,5986,4601],{"class":228},[176,5988,5989,5991,5993,5995,5997,5999,6001,6003,6005,6007,6009,6011,6014],{"class":178,"line":383},[176,5990,5880],{"class":228},[176,5992,5883],{"class":182},[176,5994,1237],{"class":228},[176,5996,5888],{"class":294},[176,5998,612],{"class":228},[176,6000,5600],{"class":182},[176,6002,408],{"class":228},[176,6004,5628],{"class":182},[176,6006,535],{"class":228},[176,6008,5901],{"class":186},[176,6010,3074],{"class":228},[176,6012,6013],{"class":186},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[176,6015,3900],{"class":228},[176,6017,6018],{"class":178,"line":389},[176,6019,5913],{"class":224},[176,6021,6022],{"class":178,"line":395},[176,6023,5918],{"class":228},[176,6025,6026],{"class":178,"line":420},[176,6027,263],{"emptyLinePlaceholder":262},[176,6029,6030,6033,6035,6038,6041,6043,6045],{"class":178,"line":426},[176,6031,6032],{"class":228},"        user, err ",[176,6034,4799],{"class":224},[176,6036,6037],{"class":228}," authService.",[176,6039,6040],{"class":182},"GetUserByToken",[176,6042,5976],{"class":228},[176,6044,1257],{"class":294},[176,6046,6047],{"class":228},"])\n",[176,6049,6050,6052,6054,6056,6058],{"class":178,"line":750},[176,6051,5865],{"class":224},[176,6053,5212],{"class":228},[176,6055,5215],{"class":224},[176,6057,5218],{"class":294},[176,6059,281],{"class":228},[176,6061,6062,6064,6066,6068,6070,6072,6074,6076,6078,6080,6082,6084,6087],{"class":178,"line":761},[176,6063,5880],{"class":228},[176,6065,5883],{"class":182},[176,6067,1237],{"class":228},[176,6069,5888],{"class":294},[176,6071,612],{"class":228},[176,6073,5600],{"class":182},[176,6075,408],{"class":228},[176,6077,5628],{"class":182},[176,6079,535],{"class":228},[176,6081,5901],{"class":186},[176,6083,3074],{"class":228},[176,6085,6086],{"class":186},"\"无效的认证令牌\"",[176,6088,3900],{"class":228},[176,6090,6091],{"class":178,"line":772},[176,6092,5913],{"class":224},[176,6094,6095],{"class":178,"line":778},[176,6096,5918],{"class":228},[176,6098,6099],{"class":178,"line":789},[176,6100,263],{"emptyLinePlaceholder":262},[176,6102,6103,6105,6108,6110,6113],{"class":178,"line":807},[176,6104,5611],{"class":228},[176,6106,6107],{"class":182},"Set",[176,6109,1237],{"class":228},[176,6111,6112],{"class":186},"\"userID\"",[176,6114,6115],{"class":228},", user.ID)\n",[176,6117,6118,6120,6122,6124,6127],{"class":178,"line":813},[176,6119,5611],{"class":228},[176,6121,6107],{"class":182},[176,6123,1237],{"class":228},[176,6125,6126],{"class":186},"\"username\"",[176,6128,6129],{"class":228},", user.Username)\n",[176,6131,6132,6134,6137],{"class":178,"line":823},[176,6133,5611],{"class":228},[176,6135,6136],{"class":182},"Next",[176,6138,3555],{"class":228},[176,6140,6141],{"class":178,"line":833},[176,6142,3329],{"class":228},[176,6144,6145],{"class":178,"line":839},[176,6146,301],{"class":228},[167,6148,6150],{"className":4773,"code":6149,"language":1522,"meta":172,"style":172},"\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",[36,6151,6152,6157,6166,6172,6185,6197,6201,6205,6223,6243,6257,6281,6303,6318,6326,6330],{"__ignoreMap":172},[176,6153,6154],{"class":178,"line":179},[176,6155,6156],{"class":247},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[176,6158,6159,6161,6164],{"class":178,"line":244},[176,6160,232],{"class":224},[176,6162,6163],{"class":182}," ctxKey",[176,6165,295],{"class":224},[176,6167,6168,6170],{"class":178,"line":251},[176,6169,312],{"class":224},[176,6171,676],{"class":228},[176,6173,6174,6177,6180,6182],{"class":178,"line":259},[176,6175,6176],{"class":294},"    ctxUserID",[176,6178,6179],{"class":182},"   ctxKey",[176,6181,1223],{"class":224},[176,6183,6184],{"class":186}," \"userID\"\n",[176,6186,6187,6190,6192,6194],{"class":178,"line":266},[176,6188,6189],{"class":294},"    ctxUsername",[176,6191,6163],{"class":182},[176,6193,1223],{"class":224},[176,6195,6196],{"class":186}," \"username\"\n",[176,6198,6199],{"class":178,"line":284},[176,6200,1243],{"class":228},[176,6202,6203],{"class":178,"line":298},[176,6204,263],{"emptyLinePlaceholder":262},[176,6206,6207,6209,6212,6215,6217,6219,6221],{"class":178,"line":304},[176,6208,4973],{"class":224},[176,6210,6211],{"class":182}," WithUserContext",[176,6213,6214],{"class":228},"() ",[176,6216,5600],{"class":182},[176,6218,408],{"class":228},[176,6220,5817],{"class":182},[176,6222,281],{"class":228},[176,6224,6225,6227,6229,6231,6233,6235,6237,6239,6241],{"class":178,"line":309},[176,6226,5029],{"class":224},[176,6228,5826],{"class":224},[176,6230,1237],{"class":228},[176,6232,5594],{"class":287},[176,6234,5597],{"class":224},[176,6236,5600],{"class":182},[176,6238,408],{"class":228},[176,6240,1054],{"class":182},[176,6242,4601],{"class":228},[176,6244,6245,6248,6250,6253,6255],{"class":178,"line":350},[176,6246,6247],{"class":228},"        ctx ",[176,6249,4799],{"class":224},[176,6251,6252],{"class":228}," c.Request.",[176,6254,1054],{"class":182},[176,6256,3555],{"class":228},[176,6258,6259,6261,6263,6266,6269,6272,6275,6277,6279],{"class":178,"line":362},[176,6260,6247],{"class":228},[176,6262,326],{"class":224},[176,6264,6265],{"class":228}," context.",[176,6267,6268],{"class":182},"WithValue",[176,6270,6271],{"class":228},"(ctx, ctxUserID, c.",[176,6273,6274],{"class":182},"GetUint",[176,6276,1237],{"class":228},[176,6278,6112],{"class":186},[176,6280,3523],{"class":228},[176,6282,6283,6285,6287,6289,6291,6294,6297,6299,6301],{"class":178,"line":383},[176,6284,6247],{"class":228},[176,6286,326],{"class":224},[176,6288,6265],{"class":228},[176,6290,6268],{"class":182},[176,6292,6293],{"class":228},"(ctx, ctxUsername, c.",[176,6295,6296],{"class":182},"GetString",[176,6298,1237],{"class":228},[176,6300,6126],{"class":186},[176,6302,3523],{"class":228},[176,6304,6305,6308,6310,6312,6315],{"class":178,"line":389},[176,6306,6307],{"class":228},"        c.Request ",[176,6309,326],{"class":224},[176,6311,6252],{"class":228},[176,6313,6314],{"class":182},"WithContext",[176,6316,6317],{"class":228},"(ctx)\n",[176,6319,6320,6322,6324],{"class":178,"line":395},[176,6321,5611],{"class":228},[176,6323,6136],{"class":182},[176,6325,3555],{"class":228},[176,6327,6328],{"class":178,"line":420},[176,6329,3329],{"class":228},[176,6331,6332],{"class":178,"line":426},[176,6333,301],{"class":228},[162,6335,6336],{"id":6336},"组合使用",[167,6338,6340],{"className":4773,"code":6339,"language":1522,"meta":172,"style":172},"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",[36,6341,6342,6354,6365,6376,6389],{"__ignoreMap":172},[176,6343,6344,6346,6348,6350,6352],{"class":178,"line":179},[176,6345,5367],{"class":228},[176,6347,5370],{"class":182},[176,6349,1237],{"class":228},[176,6351,5330],{"class":186},[176,6353,417],{"class":228},[176,6355,6356,6359,6362],{"class":178,"line":244},[176,6357,6358],{"class":182},"    AuthRequired",[176,6360,6361],{"class":228},"(authService),  ",[176,6363,6364],{"class":247},"\u002F\u002F 1. 验证 Token\n",[176,6366,6367,6370,6373],{"class":178,"line":251},[176,6368,6369],{"class":182},"    WithUserContext",[176,6371,6372],{"class":228},"(),           ",[176,6374,6375],{"class":247},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[176,6377,6378,6381,6383,6386],{"class":178,"line":259},[176,6379,6380],{"class":228},"    gin.",[176,6382,5380],{"class":182},[176,6384,6385],{"class":228},"(mcpHTTP),         ",[176,6387,6388],{"class":247},"\u002F\u002F 3. MCP 处理\n",[176,6390,6391],{"class":178,"line":266},[176,6392,1243],{"class":228},[15,6394,6395],{},"在工具处理器里就能拿到用户信息了：",[167,6397,6399],{"className":4773,"code":6398,"language":1522,"meta":172,"style":172},"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",[36,6400,6401,6456,6477,6486,6503,6507,6511,6527,6532,6545],{"__ignoreMap":172},[176,6402,6403,6405,6407,6410,6412,6415,6417,6420,6422,6424,6426,6428,6430,6432,6434,6436,6438,6440,6442,6444,6446,6448,6450,6452,6454],{"class":178,"line":179},[176,6404,4973],{"class":224},[176,6406,1062],{"class":228},[176,6408,6409],{"class":287},"h ",[176,6411,5004],{"class":224},[176,6413,6414],{"class":182},"Handler",[176,6416,341],{"class":228},[176,6418,6419],{"class":182},"handleListItems",[176,6421,1237],{"class":228},[176,6423,4978],{"class":287},[176,6425,4981],{"class":182},[176,6427,408],{"class":228},[176,6429,1054],{"class":182},[176,6431,612],{"class":228},[176,6433,4990],{"class":287},[176,6435,4993],{"class":182},[176,6437,408],{"class":228},[176,6439,4998],{"class":182},[176,6441,5001],{"class":228},[176,6443,5004],{"class":224},[176,6445,5007],{"class":182},[176,6447,408],{"class":228},[176,6449,5012],{"class":182},[176,6451,612],{"class":228},[176,6453,5017],{"class":224},[176,6455,4601],{"class":228},[176,6457,6458,6461,6463,6466,6469,6472,6475],{"class":178,"line":244},[176,6459,6460],{"class":228},"    userID, ok ",[176,6462,4799],{"class":224},[176,6464,6465],{"class":228}," ctx.",[176,6467,6468],{"class":182},"Value",[176,6470,6471],{"class":228},"(ctxUserID).(",[176,6473,6474],{"class":224},"uint",[176,6476,1243],{"class":228},[176,6478,6479,6481,6483],{"class":178,"line":251},[176,6480,3441],{"class":224},[176,6482,5967],{"class":224},[176,6484,6485],{"class":228},"ok {\n",[176,6487,6488,6490,6492,6494,6496,6499,6501],{"class":178,"line":259},[176,6489,5225],{"class":224},[176,6491,4921],{"class":228},[176,6493,5230],{"class":182},[176,6495,1237],{"class":228},[176,6497,6498],{"class":186},"\"未认证\"",[176,6500,5042],{"class":228},[176,6502,5045],{"class":294},[176,6504,6505],{"class":178,"line":266},[176,6506,3329],{"class":228},[176,6508,6509],{"class":178,"line":284},[176,6510,263],{"emptyLinePlaceholder":262},[176,6512,6513,6516,6518,6521,6524],{"class":178,"line":298},[176,6514,6515],{"class":228},"    items, _ ",[176,6517,4799],{"class":224},[176,6519,6520],{"class":228}," h.service.",[176,6522,6523],{"class":182},"ListByUser",[176,6525,6526],{"class":228},"(userID)\n",[176,6528,6529],{"class":178,"line":304},[176,6530,6531],{"class":247},"    \u002F\u002F ...\n",[176,6533,6534,6536,6538,6540,6543],{"class":178,"line":309},[176,6535,5029],{"class":224},[176,6537,4921],{"class":228},[176,6539,5034],{"class":182},[176,6541,6542],{"class":228},"(result), ",[176,6544,5045],{"class":294},[176,6546,6547],{"class":178,"line":350},[176,6548,301],{"class":228},[162,6550,6551],{"id":6551},"客户端配置",[15,6553,6554],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[167,6556,6560],{"className":6557,"code":6558,"language":6559,"meta":172,"style":172},"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",[36,6561,6562,6567,6574,6580,6592,6599,6609,6614,6618,6622],{"__ignoreMap":172},[176,6563,6564],{"class":178,"line":179},[176,6565,6566],{"class":228},"{\n",[176,6568,6569,6572],{"class":178,"line":244},[176,6570,6571],{"class":294},"  \"mcpServers\"",[176,6573,3288],{"class":228},[176,6575,6576,6578],{"class":178,"line":251},[176,6577,4813],{"class":294},[176,6579,3288],{"class":228},[176,6581,6582,6585,6587,6590],{"class":178,"line":259},[176,6583,6584],{"class":294},"      \"url\"",[176,6586,3074],{"class":228},[176,6588,6589],{"class":186},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[176,6591,417],{"class":228},[176,6593,6594,6597],{"class":178,"line":266},[176,6595,6596],{"class":294},"      \"headers\"",[176,6598,3288],{"class":228},[176,6600,6601,6604,6606],{"class":178,"line":284},[176,6602,6603],{"class":294},"        \"Authorization\"",[176,6605,3074],{"class":228},[176,6607,6608],{"class":186},"\"Bearer your-token-here\"\n",[176,6610,6611],{"class":178,"line":298},[176,6612,6613],{"class":228},"      }\n",[176,6615,6616],{"class":178,"line":304},[176,6617,3329],{"class":228},[176,6619,6620],{"class":178,"line":309},[176,6621,3334],{"class":228},[176,6623,6624],{"class":178,"line":350},[176,6625,301],{"class":228},[11,6627,2534],{"id":2534},[48,6629,6630,6639],{},[51,6631,6632],{},[54,6633,6634,6637],{},[57,6635,6636],{},"步骤",[57,6638,68],{},[70,6640,6641,6650,6661,6670,6679],{},[54,6642,6643,6647],{},[75,6644,6645],{},[36,6646,4805],{},[75,6648,6649],{},"创建 MCP 协议实例",[54,6651,6652,6658],{},[75,6653,6654,2551,6656],{},[36,6655,4924],{},[36,6657,4967],{},[75,6659,6660],{},"定义并注册工具",[54,6662,6663,6667],{},[75,6664,6665],{},[36,6666,5315],{},[75,6668,6669],{},"转为 HTTP Handler",[54,6671,6672,6676],{},[75,6673,6674],{},[36,6675,5283],{},[75,6677,6678],{},"挂载到 Gin 路由",[54,6680,6681,6689],{},[75,6682,6683,2551,6686],{},[36,6684,6685],{},"AuthRequired",[36,6687,6688],{},"WithUserContext",[75,6690,6691],{},"鉴权 + 用户信息桥接",[15,6693,6694,6695,6698,6699,910],{},"核心就一句话：",[25,6696,6697],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[36,6700,5781],{},[1346,6702,6703],{},"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":172,"searchDepth":244,"depth":251,"links":6705},[6706,6707,6708,6709,6716,6722],{"id":4685,"depth":244,"text":4686},{"id":4695,"depth":244,"text":4696},{"id":4712,"depth":244,"text":4712},{"id":4738,"depth":244,"text":4738,"children":6710},[6711,6712,6713,6714,6715],{"id":4741,"depth":251,"text":4742},{"id":4769,"depth":251,"text":4770},{"id":4885,"depth":251,"text":4886},{"id":5272,"depth":251,"text":5273},{"id":5393,"depth":251,"text":5394},{"id":5754,"depth":244,"text":5755,"children":6717},[6718,6719,6720,6721],{"id":4327,"depth":251,"text":4327},{"id":5774,"depth":251,"text":5775},{"id":6336,"depth":251,"text":6336},{"id":6551,"depth":251,"text":6551},{"id":2534,"depth":244,"text":2534},"介绍如何使用 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":4680,"description":6723},"Go 工程实践","posts\u002Fgo-mcp-gin\u002Findex",[2179,6732,6733,6734,6735],"MCP","Gin","AI Agent","后端","2026-06-01","5fZl1Vigs2lHb5fHPz-0YZFyFD38ws1heOouZUG1g6M",{"id":6739,"title":6740,"author":6,"body":6741,"description":7507,"difficulty":6,"draft":1368,"extension":1369,"image":7508,"meta":7509,"navigation":262,"path":7510,"pinned":1368,"published":7511,"seo":7512,"series":6,"seriesOrder":6,"stem":7513,"tags":7514,"updated":6,"__hash__":7518},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":8,"value":6742,"toc":7493},[6743,6747,6750,6757,6883,6886,6889,6944,6947,6962,6965,6968,6975,7207,7214,7218,7221,7241,7244,7259,7262,7282,7285,7288,7295,7361,7364,7367,7371,7374,7426,7430,7451,7455,7458,7472,7475,7479,7482,7484,7487,7490],[11,6744,6746],{"id":6745},"dws-是什么","dws 是什么",[15,6748,6749],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[15,6751,6752,6753,6756],{},"它的定位很明确：",[25,6754,6755],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[167,6758,6760],{"className":169,"code":6759,"language":171,"meta":172,"style":172},"# 查看今天有什么会\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",[36,6761,6762,6767,6793,6797,6802,6824,6828,6833,6858,6862,6867],{"__ignoreMap":172},[176,6763,6764],{"class":178,"line":179},[176,6765,6766],{"class":247},"# 查看今天有什么会\n",[176,6768,6769,6772,6775,6778,6781,6784,6787,6790],{"class":178,"line":244},[176,6770,6771],{"class":182},"dws",[176,6773,6774],{"class":186}," calendar",[176,6776,6777],{"class":186}," event",[176,6779,6780],{"class":186}," list",[176,6782,6783],{"class":294}," --start",[176,6785,6786],{"class":186}," 2026-06-01",[176,6788,6789],{"class":294}," --end",[176,6791,6792],{"class":186}," 2026-06-01\n",[176,6794,6795],{"class":178,"line":251},[176,6796,263],{"emptyLinePlaceholder":262},[176,6798,6799],{"class":178,"line":259},[176,6800,6801],{"class":247},"# 给群里发条消息\n",[176,6803,6804,6806,6809,6812,6815,6818,6821],{"class":178,"line":266},[176,6805,6771],{"class":182},[176,6807,6808],{"class":186}," chat",[176,6810,6811],{"class":186}," send",[176,6813,6814],{"class":294}," --group-id",[176,6816,6817],{"class":186}," xxx",[176,6819,6820],{"class":294}," --content",[176,6822,6823],{"class":186}," \"今天发布推迟到下午3点\"\n",[176,6825,6826],{"class":178,"line":284},[176,6827,263],{"emptyLinePlaceholder":262},[176,6829,6830],{"class":178,"line":298},[176,6831,6832],{"class":247},"# 创建一个待办\n",[176,6834,6835,6837,6840,6843,6846,6849,6852,6855],{"class":178,"line":304},[176,6836,6771],{"class":182},[176,6838,6839],{"class":186}," todo",[176,6841,6842],{"class":186}," task",[176,6844,6845],{"class":186}," create",[176,6847,6848],{"class":294}," --subject",[176,6850,6851],{"class":186}," \"Review PR #123\"",[176,6853,6854],{"class":294}," --due-date",[176,6856,6857],{"class":186}," 2026-06-02\n",[176,6859,6860],{"class":178,"line":309},[176,6861,263],{"emptyLinePlaceholder":262},[176,6863,6864],{"class":178,"line":350},[176,6865,6866],{"class":247},"# 读取钉钉文档\n",[176,6868,6869,6871,6874,6877,6880],{"class":178,"line":362},[176,6870,6771],{"class":182},[176,6872,6873],{"class":186}," doc",[176,6875,6876],{"class":186}," read",[176,6878,6879],{"class":294}," --url",[176,6881,6882],{"class":186}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[11,6884,6885],{"id":6885},"安装",[15,6887,6888],{},"三种方式，选你喜欢的：",[167,6890,6892],{"className":169,"code":6891,"language":171,"meta":172,"style":172},"# 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",[36,6893,6894,6899,6911,6915,6920,6930,6934,6939],{"__ignoreMap":172},[176,6895,6896],{"class":178,"line":179},[176,6897,6898],{"class":247},"# npm（推荐，跨平台自动下载二进制）\n",[176,6900,6901,6903,6905,6908],{"class":178,"line":244},[176,6902,183],{"class":182},[176,6904,187],{"class":186},[176,6906,6907],{"class":294}," -g",[176,6909,6910],{"class":186}," dingtalk-workspace-cli\n",[176,6912,6913],{"class":178,"line":251},[176,6914,263],{"emptyLinePlaceholder":262},[176,6916,6917],{"class":178,"line":259},[176,6918,6919],{"class":247},"# Homebrew（macOS\u002FLinux）\n",[176,6921,6922,6925,6927],{"class":178,"line":266},[176,6923,6924],{"class":182},"brew",[176,6926,187],{"class":186},[176,6928,6929],{"class":186}," dws\n",[176,6931,6932],{"class":178,"line":284},[176,6933,263],{"emptyLinePlaceholder":262},[176,6935,6936],{"class":178,"line":298},[176,6937,6938],{"class":247},"# 直接下载二进制\n",[176,6940,6941],{"class":178,"line":304},[176,6942,6943],{"class":247},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[15,6945,6946],{},"安装后认证：",[167,6948,6950],{"className":169,"code":6949,"language":171,"meta":172,"style":172},"dws auth login\n",[36,6951,6952],{"__ignoreMap":172},[176,6953,6954,6956,6959],{"class":178,"line":179},[176,6955,6771],{"class":182},[176,6957,6958],{"class":186}," auth",[176,6960,6961],{"class":186}," login\n",[15,6963,6964],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[11,6966,6967],{"id":6967},"覆盖的钉钉产品",[15,6969,6970,6971,6974],{},"这是最夸张的部分 — ",[25,6972,6973],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[48,6976,6977,6989],{},[51,6978,6979],{},[54,6980,6981,6984,6987],{},[57,6982,6983],{},"产品",[57,6985,6986],{},"命令数",[57,6988,68],{},[70,6990,6991,7005,7019,7033,7047,7061,7075,7089,7102,7116,7129,7143,7157,7170,7183,7196],{},[54,6992,6993,6999,7002],{},[75,6994,6995,6996],{},"聊天 ",[36,6997,6998],{},"chat",[75,7000,7001],{},"57",[75,7003,7004],{},"消息收发、群管理、机器人、已读状态、Webhook",[54,7006,7007,7013,7016],{},[75,7008,7009,7010],{},"AI 表格 ",[36,7011,7012],{},"aitable",[75,7014,7015],{},"42",[75,7017,7018],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[54,7020,7021,7027,7030],{},[75,7022,7023,7024],{},"电子表格 ",[36,7025,7026],{},"sheet",[75,7028,7029],{},"34",[75,7031,7032],{},"工作表读写、行列操作、合并、筛选视图、导出",[54,7034,7035,7041,7044],{},[75,7036,7037,7038],{},"文档 ",[36,7039,7040],{},"doc",[75,7042,7043],{},"21",[75,7045,7046],{},"搜索、读写、块级编辑、评论、复制移动",[54,7048,7049,7055,7058],{},[75,7050,7051,7052],{},"会议纪要 ",[36,7053,7054],{},"minutes",[75,7056,7057],{},"19",[75,7059,7060],{},"列表、摘要、转写、思维导图、发言人、热词",[54,7062,7063,7069,7072],{},[75,7064,7065,7066],{},"日历 ",[36,7067,7068],{},"calendar",[75,7070,7071],{},"14",[75,7073,7074],{},"事件 CRUD、会议室、忙闲查询、时间建议",[54,7076,7077,7083,7086],{},[75,7078,7079,7080],{},"审批 ",[36,7081,7082],{},"oa",[75,7084,7085],{},"9",[75,7087,7088],{},"待办、发起、同意\u002F拒绝\u002F撤销",[54,7090,7091,7097,7099],{},[75,7092,7093,7094],{},"云盘 ",[36,7095,7096],{},"drive",[75,7098,7085],{},[75,7100,7101],{},"列表、下载、上传、创建目录",[54,7103,7104,7110,7113],{},[75,7105,7106,7107],{},"知识库 ",[36,7108,7109],{},"wiki",[75,7111,7112],{},"7",[75,7114,7115],{},"空间管理、成员管理",[54,7117,7118,7124,7126],{},[75,7119,7120,7121],{},"汇报 ",[36,7122,7123],{},"report",[75,7125,7112],{},[75,7127,7128],{},"日志\u002F日报\u002F周报，按模板创建、统计",[54,7130,7131,7137,7140],{},[75,7132,7133,7134],{},"待办 ",[36,7135,7136],{},"todo",[75,7138,7139],{},"6",[75,7141,7142],{},"创建、完成、删除，支持优先级和循环",[54,7144,7145,7151,7154],{},[75,7146,7147,7148],{},"邮件 ",[36,7149,7150],{},"mail",[75,7152,7153],{},"4",[75,7155,7156],{},"邮箱查询、邮件搜索、发送",[54,7158,7159,7165,7167],{},[75,7160,7161,7162],{},"考勤 ",[36,7163,7164],{},"attendance",[75,7166,7153],{},[75,7168,7169],{},"打卡记录、排班、汇总",[54,7171,7172,7178,7180],{},[75,7173,7174,7175],{},"通讯录 ",[36,7176,7177],{},"contact",[75,7179,3318],{},[75,7181,7182],{},"用户搜索、部门查询",[54,7184,7185,7191,7193],{},[75,7186,7187,7188],{},"DING 消息 ",[36,7189,7190],{},"ding",[75,7192,3546],{},[75,7194,7195],{},"发送\u002F撤回（应用内、短信、电话）",[54,7197,7198,7201,7204],{},[75,7199,7200],{},"其他",[75,7202,7203],{},"若干",[75,7205,7206],{},"AI 搜索、AI 应用、直播、开放平台文档",[15,7208,7209,7210,7213],{},"还有一个万能命令 ",[36,7211,7212],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[11,7215,7217],{"id":7216},"agent-skill-系统","Agent Skill 系统",[15,7219,7220],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[859,7222,7223,7229,7235],{},[22,7224,7225,7228],{},[25,7226,7227],{},"Claude Code"," — 通过 SKILL.md 自动加载",[22,7230,7231,7234],{},[25,7232,7233],{},"Cursor"," — 支持 MCP 协议接入",[22,7236,7237,7240],{},[25,7238,7239],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[15,7242,7243],{},"安装 skill：",[167,7245,7247],{"className":169,"code":7246,"language":171,"meta":172,"style":172},"dws skill setup\n",[36,7248,7249],{"__ignoreMap":172},[176,7250,7251,7253,7256],{"class":178,"line":179},[176,7252,6771],{"class":182},[176,7254,7255],{"class":186}," skill",[176,7257,7258],{"class":186}," setup\n",[15,7260,7261],{},"SKILL.md 里定义了：",[859,7263,7264,7273,7276,7279],{},[22,7265,7266,7267,7269,7270,7272],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[36,7268,7012],{},"，说\"打卡\"→ ",[36,7271,7164],{},"）",[22,7274,7275],{},"危险操作确认流程（删除、撤回等必须先确认）",[22,7277,7278],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[22,7280,7281],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[15,7283,7284],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[11,7286,7287],{"id":7287},"自动升级",[15,7289,7290,7291,7294],{},"内置 ",[36,7292,7293],{},"dws upgrade","，不需要重新下载安装包：",[167,7296,7298],{"className":169,"code":7297,"language":171,"meta":172,"style":172},"dws upgrade              # 交互式升级到最新版\ndws upgrade --check      # 仅检查是否有新版本\ndws upgrade --list       # 列出最近版本\ndws upgrade --version v1.0.5  # 升级到指定版本\ndws upgrade --rollback   # 回滚到上一版本\n",[36,7299,7300,7310,7322,7334,7349],{"__ignoreMap":172},[176,7301,7302,7304,7307],{"class":178,"line":179},[176,7303,6771],{"class":182},[176,7305,7306],{"class":186}," upgrade",[176,7308,7309],{"class":247},"              # 交互式升级到最新版\n",[176,7311,7312,7314,7316,7319],{"class":178,"line":244},[176,7313,6771],{"class":182},[176,7315,7306],{"class":186},[176,7317,7318],{"class":294}," --check",[176,7320,7321],{"class":247},"      # 仅检查是否有新版本\n",[176,7323,7324,7326,7328,7331],{"class":178,"line":251},[176,7325,6771],{"class":182},[176,7327,7306],{"class":186},[176,7329,7330],{"class":294}," --list",[176,7332,7333],{"class":247},"       # 列出最近版本\n",[176,7335,7336,7338,7340,7343,7346],{"class":178,"line":259},[176,7337,6771],{"class":182},[176,7339,7306],{"class":186},[176,7341,7342],{"class":294}," --version",[176,7344,7345],{"class":186}," v1.0.5",[176,7347,7348],{"class":247},"  # 升级到指定版本\n",[176,7350,7351,7353,7355,7358],{"class":178,"line":266},[176,7352,6771],{"class":182},[176,7354,7306],{"class":186},[176,7356,7357],{"class":294}," --rollback",[176,7359,7360],{"class":247},"   # 回滚到上一版本\n",[15,7362,7363],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[11,7365,7366],{"id":7366},"亮点",[162,7368,7370],{"id":7369},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[15,7372,7373],{},"这可能是目前对 AI 最友好的 CLI 工具：",[859,7375,7376,7397,7407,7416],{},[22,7377,7378,7381,7382,7385,7386,7389,7390,7385,7393,7396],{},[25,7379,7380],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[36,7383,7384],{},"--userId"," 应该是 ",[36,7387,7388],{},"--user-id","，",[36,7391,7392],{},"--limit100",[36,7394,7395],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[22,7398,7399,7402,7403,7406],{},[25,7400,7401],{},"Schema 发现"," — ",[36,7404,7405],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[22,7408,7409,7402,7412,7415],{},[25,7410,7411],{},"jq 过滤",[36,7413,7414],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[22,7417,7418,7421,7422,7425],{},[25,7419,7420],{},"JSON 输出"," — 所有命令支持 ",[36,7423,7424],{},"--format json","，方便程序解析",[162,7427,7429],{"id":7428},"_2-安全设计","2. 安全设计",[859,7431,7432,7435,7438,7441,7448],{},[22,7433,7434],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[22,7436,7437],{},"域名白名单，只能访问钉钉官方 API",[22,7439,7440],{},"强制 HTTPS",[22,7442,7443,7444,7447],{},"写操作必须显式 ",[36,7445,7446],{},"--yes"," 确认",[22,7449,7450],{},"完整审计链",[162,7452,7454],{"id":7453},"_3-发现驱动架构","3. 发现驱动架构",[15,7456,7457],{},"不是把所有命令硬编码在代码里，而是：",[19,7459,7460,7463,7466,7469],{},[22,7461,7462],{},"从钉钉 MCP 服务器注册中心拉取元数据",[22,7464,7465],{},"运行时发现服务端能力",[22,7467,7468],{},"转换成统一的中间表示（IR）",[22,7470,7471],{},"动态挂载到命令树",[15,7473,7474],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[162,7476,7478],{"id":7477},"_4-多平台分发","4. 多平台分发",[15,7480,7481],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[11,7483,2534],{"id":2534},[15,7485,7486],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[15,7488,7489],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[1346,7491,7492],{},"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":172,"searchDepth":244,"depth":251,"links":7494},[7495,7496,7497,7498,7499,7500,7506],{"id":6745,"depth":244,"text":6746},{"id":6885,"depth":244,"text":6885},{"id":6967,"depth":244,"text":6967},{"id":7216,"depth":244,"text":7217},{"id":7287,"depth":244,"text":7287},{"id":7366,"depth":244,"text":7366,"children":7501},[7502,7503,7504,7505],{"id":7369,"depth":251,"text":7370},{"id":7428,"depth":251,"text":7429},{"id":7453,"depth":251,"text":7454},{"id":7477,"depth":251,"text":7478},{"id":2534,"depth":244,"text":2534},"介绍阿里巴巴官方的 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":6740,"description":7507},"posts\u002Fdws-dingtalk-cli\u002Findex",[7515,7516,6734,2179,7517],"钉钉","CLI","效率工具","4ca65VUZ2Sz9yG8dNtftrBDDHE6QowA3ed9g4egQkKI",{"id":7520,"title":7521,"author":6,"body":7522,"description":8481,"difficulty":2169,"draft":1368,"extension":1369,"image":8482,"meta":8483,"navigation":262,"path":8484,"pinned":1368,"published":8485,"seo":8486,"series":6729,"seriesOrder":244,"stem":8487,"tags":8488,"updated":8492,"__hash__":8493},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",{"type":8,"value":7523,"toc":8464},[7524,7526,7533,7536,7539,7550,7553,7557,7890,7894,8002,8006,8140,8143,8147,8156,8171,8182,8200,8214,8220,8228,8326,8332,8336,8343,8352,8355,8391,8394,8399,8438,8440,8446,8458,8461],[11,7525,3237],{"id":3237},[15,7527,7528,7529,7532],{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[36,7530,7531],{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[15,7534,7535],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[11,7537,7538],{"id":7538},"核心思路",[15,7540,7541,7542,7545,7546,7549],{},"利用 Go 1.16 引入的 ",[36,7543,7544],{},"\u002F\u002Fgo:embed"," 指令，在编译时将前端 ",[36,7547,7548],{},"dist\u002F"," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[11,7551,7552],{"id":7552},"逐步实现",[162,7554,7556],{"id":7555},"_1-创建嵌入模块","1. 创建嵌入模块",[167,7558,7560],{"className":4773,"code":7559,"language":1522,"meta":172,"style":172},"\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",[36,7561,7562,7567,7575,7579,7585,7595,7604,7613,7622,7631,7640,7644,7653,7657,7661,7666,7681,7685,7702,7723,7727,7747,7767,7780,7790,7794,7798,7813,7825,7830,7849,7853,7857,7880,7885],{"__ignoreMap":172},[176,7563,7564],{"class":178,"line":179},[176,7565,7566],{"class":247},"\u002F\u002F service\u002Fweb\u002Fembed.go\n",[176,7568,7569,7572],{"class":178,"line":244},[176,7570,7571],{"class":224},"package",[176,7573,7574],{"class":182}," web\n",[176,7576,7577],{"class":178,"line":251},[176,7578,263],{"emptyLinePlaceholder":262},[176,7580,7581,7583],{"class":178,"line":259},[176,7582,225],{"class":224},[176,7584,676],{"class":228},[176,7586,7587,7590,7593],{"class":178,"line":266},[176,7588,7589],{"class":186},"    \"",[176,7591,7592],{"class":182},"embed",[176,7594,1619],{"class":186},[176,7596,7597,7599,7602],{"class":178,"line":284},[176,7598,7589],{"class":186},[176,7600,7601],{"class":182},"io\u002Ffs",[176,7603,1619],{"class":186},[176,7605,7606,7608,7611],{"class":178,"line":298},[176,7607,7589],{"class":186},[176,7609,7610],{"class":182},"mime",[176,7612,1619],{"class":186},[176,7614,7615,7617,7620],{"class":178,"line":304},[176,7616,7589],{"class":186},[176,7618,7619],{"class":182},"net\u002Fhttp",[176,7621,1619],{"class":186},[176,7623,7624,7626,7629],{"class":178,"line":309},[176,7625,7589],{"class":186},[176,7627,7628],{"class":182},"path\u002Ffilepath",[176,7630,1619],{"class":186},[176,7632,7633,7635,7638],{"class":178,"line":350},[176,7634,7589],{"class":186},[176,7636,7637],{"class":182},"strings",[176,7639,1619],{"class":186},[176,7641,7642],{"class":178,"line":362},[176,7643,263],{"emptyLinePlaceholder":262},[176,7645,7646,7648,7651],{"class":178,"line":383},[176,7647,7589],{"class":186},[176,7649,7650],{"class":182},"github.com\u002Fgin-gonic\u002Fgin",[176,7652,1619],{"class":186},[176,7654,7655],{"class":178,"line":389},[176,7656,1243],{"class":228},[176,7658,7659],{"class":178,"line":395},[176,7660,263],{"emptyLinePlaceholder":262},[176,7662,7663],{"class":178,"line":420},[176,7664,7665],{"class":247},"\u002F\u002Fgo:embed dist\n",[176,7667,7668,7671,7674,7676,7678],{"class":178,"line":426},[176,7669,7670],{"class":224},"var",[176,7672,7673],{"class":228}," embedFS ",[176,7675,7592],{"class":182},[176,7677,408],{"class":228},[176,7679,7680],{"class":182},"FS\n",[176,7682,7683],{"class":178,"line":750},[176,7684,263],{"emptyLinePlaceholder":262},[176,7686,7687,7689,7692,7694,7696,7698,7700],{"class":178,"line":761},[176,7688,4973],{"class":224},[176,7690,7691],{"class":182}," SPAHandler",[176,7693,6214],{"class":228},[176,7695,5600],{"class":182},[176,7697,408],{"class":228},[176,7699,5817],{"class":182},[176,7701,281],{"class":228},[176,7703,7704,7707,7709,7712,7715,7718,7721],{"class":178,"line":772},[176,7705,7706],{"class":228},"    sub, _ ",[176,7708,4799],{"class":224},[176,7710,7711],{"class":228}," fs.",[176,7713,7714],{"class":182},"Sub",[176,7716,7717],{"class":228},"(embedFS, ",[176,7719,7720],{"class":186},"\"dist\"",[176,7722,1243],{"class":228},[176,7724,7725],{"class":178,"line":778},[176,7726,263],{"emptyLinePlaceholder":262},[176,7728,7729,7731,7733,7735,7737,7739,7741,7743,7745],{"class":178,"line":789},[176,7730,5029],{"class":224},[176,7732,5826],{"class":224},[176,7734,1237],{"class":228},[176,7736,5594],{"class":287},[176,7738,5597],{"class":224},[176,7740,5600],{"class":182},[176,7742,408],{"class":228},[176,7744,1054],{"class":182},[176,7746,4601],{"class":228},[176,7748,7749,7752,7754,7756,7759,7762,7765],{"class":178,"line":807},[176,7750,7751],{"class":228},"        p ",[176,7753,4799],{"class":224},[176,7755,5932],{"class":228},[176,7757,7758],{"class":182},"TrimPrefix",[176,7760,7761],{"class":228},"(c.Request.URL.Path, ",[176,7763,7764],{"class":186},"\"\u002F\"",[176,7766,1243],{"class":228},[176,7768,7769,7771,7774,7776,7778],{"class":178,"line":813},[176,7770,5865],{"class":224},[176,7772,7773],{"class":228}," p ",[176,7775,5871],{"class":224},[176,7777,745],{"class":186},[176,7779,281],{"class":228},[176,7781,7782,7785,7787],{"class":178,"line":823},[176,7783,7784],{"class":228},"            p ",[176,7786,326],{"class":224},[176,7788,7789],{"class":186}," \"index.html\"\n",[176,7791,7792],{"class":178,"line":833},[176,7793,5918],{"class":228},[176,7795,7796],{"class":178,"line":839},[176,7797,263],{"emptyLinePlaceholder":262},[176,7799,7800,7803,7805,7807,7810],{"class":178,"line":845},[176,7801,7802],{"class":228},"        data, err ",[176,7804,4799],{"class":224},[176,7806,7711],{"class":228},[176,7808,7809],{"class":182},"ReadFile",[176,7811,7812],{"class":228},"(sub, p)\n",[176,7814,7815,7817,7819,7821,7823],{"class":178,"line":851},[176,7816,5865],{"class":224},[176,7818,5212],{"class":228},[176,7820,5215],{"class":224},[176,7822,5218],{"class":294},[176,7824,281],{"class":228},[176,7826,7827],{"class":178,"line":2493},[176,7828,7829],{"class":247},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[176,7831,7832,7835,7837,7839,7841,7844,7847],{"class":178,"line":2498},[176,7833,7834],{"class":228},"            data, _ ",[176,7836,326],{"class":224},[176,7838,7711],{"class":228},[176,7840,7809],{"class":182},[176,7842,7843],{"class":228},"(sub, ",[176,7845,7846],{"class":186},"\"index.html\"",[176,7848,1243],{"class":228},[176,7850,7851],{"class":178,"line":2504},[176,7852,5918],{"class":228},[176,7854,7855],{"class":178,"line":2513},[176,7856,263],{"emptyLinePlaceholder":262},[176,7858,7860,7862,7865,7868,7871,7874,7877],{"class":178,"line":7859},33,[176,7861,5611],{"class":228},[176,7863,7864],{"class":182},"Data",[176,7866,7867],{"class":228},"(http.StatusOK, mime.",[176,7869,7870],{"class":182},"TypeByExtension",[176,7872,7873],{"class":228},"(filepath.",[176,7875,7876],{"class":182},"Ext",[176,7878,7879],{"class":228},"(p)), data)\n",[176,7881,7883],{"class":178,"line":7882},34,[176,7884,3329],{"class":228},[176,7886,7888],{"class":178,"line":7887},35,[176,7889,301],{"class":228},[162,7891,7893],{"id":7892},"_2-改造路由","2. 改造路由",[167,7895,7897],{"className":4773,"code":7896,"language":1522,"meta":172,"style":172},"\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",[36,7898,7899,7904,7915,7919,7924,7939,7953,7966,7979,7992],{"__ignoreMap":172},[176,7900,7901],{"class":178,"line":179},[176,7902,7903],{"class":247},"\u002F\u002F router.go\n",[176,7905,7906,7908,7910,7913],{"class":178,"line":244},[176,7907,225],{"class":224},[176,7909,1428],{"class":186},[176,7911,7912],{"class":182},"myproject\u002Fweb",[176,7914,1619],{"class":186},[176,7916,7917],{"class":178,"line":251},[176,7918,263],{"emptyLinePlaceholder":262},[176,7920,7921],{"class":178,"line":259},[176,7922,7923],{"class":247},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[176,7925,7926,7929,7931,7934,7937],{"class":178,"line":266},[176,7927,7928],{"class":228},"webHandler ",[176,7930,4799],{"class":224},[176,7932,7933],{"class":228}," web.",[176,7935,7936],{"class":182},"SPAHandler",[176,7938,3555],{"class":228},[176,7940,7941,7944,7946,7948,7950],{"class":178,"line":284},[176,7942,7943],{"class":228},"router.",[176,7945,5580],{"class":182},[176,7947,1237],{"class":228},[176,7949,7764],{"class":186},[176,7951,7952],{"class":228},", webHandler)\n",[176,7954,7955,7957,7959,7961,7964],{"class":178,"line":298},[176,7956,7943],{"class":228},[176,7958,5580],{"class":182},[176,7960,1237],{"class":228},[176,7962,7963],{"class":186},"\"\u002Fassets\u002F*filepath\"",[176,7965,7952],{"class":228},[176,7967,7968,7970,7972,7974,7977],{"class":178,"line":304},[176,7969,7943],{"class":228},[176,7971,5580],{"class":182},[176,7973,1237],{"class":228},[176,7975,7976],{"class":186},"\"\u002Ffavicon.ico\"",[176,7978,7952],{"class":228},[176,7980,7981,7983,7985,7987,7990],{"class":178,"line":309},[176,7982,7943],{"class":228},[176,7984,5580],{"class":182},[176,7986,1237],{"class":228},[176,7988,7989],{"class":186},"\"\u002Ffavicon.svg\"",[176,7991,7952],{"class":228},[176,7993,7994,7996,7999],{"class":178,"line":350},[176,7995,7943],{"class":228},[176,7997,7998],{"class":182},"NoRoute",[176,8000,8001],{"class":228},"(webHandler)\n",[162,8003,8005],{"id":8004},"_3-一键构建脚本","3. 一键构建脚本",[167,8007,8009],{"className":169,"code":8008,"language":171,"meta":172,"style":172},"#!\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",[36,8010,8011,8016,8021,8025,8033,8037,8042,8052,8062,8066,8071,8082,8095,8099,8104,8112,8125,8135],{"__ignoreMap":172},[176,8012,8013],{"class":178,"line":179},[176,8014,8015],{"class":247},"#!\u002Fbin\u002Fbash\n",[176,8017,8018],{"class":178,"line":244},[176,8019,8020],{"class":247},"# build-standalone.sh\n",[176,8022,8023],{"class":178,"line":251},[176,8024,263],{"emptyLinePlaceholder":262},[176,8026,8027,8030],{"class":178,"line":259},[176,8028,8029],{"class":294},"set",[176,8031,8032],{"class":294}," -e\n",[176,8034,8035],{"class":178,"line":266},[176,8036,263],{"emptyLinePlaceholder":262},[176,8038,8039],{"class":178,"line":284},[176,8040,8041],{"class":247},"# 1. 构建前端\n",[176,8043,8044,8047,8049],{"class":178,"line":298},[176,8045,8046],{"class":182},"pnpm",[176,8048,187],{"class":186},[176,8050,8051],{"class":294}," --no-frozen-lockfile\n",[176,8053,8054,8056,8059],{"class":178,"line":304},[176,8055,3135],{"class":182},[176,8057,8058],{"class":186}," vite",[176,8060,8061],{"class":186}," build\n",[176,8063,8064],{"class":178,"line":309},[176,8065,263],{"emptyLinePlaceholder":262},[176,8067,8068],{"class":178,"line":350},[176,8069,8070],{"class":247},"# 2. 复制到嵌入目录\n",[176,8072,8073,8076,8079],{"class":178,"line":362},[176,8074,8075],{"class":182},"rm",[176,8077,8078],{"class":294}," -rf",[176,8080,8081],{"class":186}," service\u002Fweb\u002Fdist\n",[176,8083,8084,8087,8090,8093],{"class":178,"line":383},[176,8085,8086],{"class":182},"cp",[176,8088,8089],{"class":294}," -r",[176,8091,8092],{"class":186}," dist",[176,8094,8081],{"class":186},[176,8096,8097],{"class":178,"line":389},[176,8098,263],{"emptyLinePlaceholder":262},[176,8100,8101],{"class":178,"line":395},[176,8102,8103],{"class":247},"# 3. 编译 Go 二进制\n",[176,8105,8106,8109],{"class":178,"line":420},[176,8107,8108],{"class":294},"cd",[176,8110,8111],{"class":186}," service\n",[176,8113,8114,8116,8118,8120,8123],{"class":178,"line":426},[176,8115,1522],{"class":182},[176,8117,1537],{"class":186},[176,8119,1546],{"class":294},[176,8121,8122],{"class":186}," myapp.exe",[176,8124,5697],{"class":294},[176,8126,8127,8130,8133],{"class":178,"line":750},[176,8128,8129],{"class":294},"    -ldflags=",[176,8131,8132],{"class":186},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[176,8134,5697],{"class":294},[176,8136,8137],{"class":178,"line":761},[176,8138,8139],{"class":186},"    main.go\n",[11,8141,8142],{"id":8142},"踩坑记录",[162,8144,8146],{"id":8145},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[15,8148,8149,8150,8152,8153,8155],{},"首次实现时用了 Gin 的 ",[36,8151,7998],{}," 兜底，但部分静态资源返回 404。排查发现 ",[36,8154,7998],{}," 在某些路由未显式注册时不会按预期触发。",[15,8157,8158,8160,8161,205,8164,8167,8168,8170],{},[25,8159,946],{},"：显式注册 ",[36,8162,8163],{},"\u002Fassets\u002F*filepath",[36,8165,8166],{},"\u002Ffavicon.ico"," 等路由，",[36,8169,7998],{}," 仅作为兜底。",[162,8172,8174,8175,8177,8178,8181],{"id":8173},"坑二goembed-排除-_-开头文件","坑二：",[36,8176,7544],{}," 排除 ",[36,8179,8180],{},"_"," 开头文件",[15,8183,8184,8185,8188,8189,8191,8192,910],{},"这是最坑的。Vite 构建产出了一个 ",[36,8186,8187],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[36,8190,7544],{}," 当模式为目录时，",[25,8193,8194,8195,1193,8197,8199],{},"硬编码排除了 ",[36,8196,8180],{},[36,8198,408],{}," 开头的文件",[167,8201,8203],{"className":4773,"code":8202,"language":1522,"meta":172,"style":172},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[36,8204,8205,8210],{"__ignoreMap":172},[176,8206,8207],{"class":178,"line":179},[176,8208,8209],{"class":247},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[176,8211,8212],{"class":178,"line":244},[176,8213,7665],{"class":247},[15,8215,8216,8217,910],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[36,8218,8219],{},"Unexpected token '\u003C'",[15,8221,8222,8224,8225,8227],{},[25,8223,946],{},"：改 Vite 配置，去除了 ",[36,8226,8180],{}," 前缀：",[167,8229,8233],{"className":8230,"code":8231,"language":8232,"meta":172,"style":172},"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",[36,8234,8235,8240,8247,8254,8261,8269,8307,8312,8316,8321],{"__ignoreMap":172},[176,8236,8237],{"class":178,"line":179},[176,8238,8239],{"class":247},"\u002F\u002F vite.config.ts\n",[176,8241,8242,8245],{"class":178,"line":244},[176,8243,8244],{"class":182},"build",[176,8246,3288],{"class":228},[176,8248,8249,8252],{"class":178,"line":251},[176,8250,8251],{"class":182},"  rollupOptions",[176,8253,3288],{"class":228},[176,8255,8256,8259],{"class":178,"line":259},[176,8257,8258],{"class":182},"    output",[176,8260,3288],{"class":228},[176,8262,8263,8266],{"class":178,"line":266},[176,8264,8265],{"class":182},"      chunkFileNames",[176,8267,8268],{"class":228},"(chunkInfo) {\n",[176,8270,8271,8273,8276,8279,8281,8284,8286,8288,8290,8292,8294,8296,8298,8300,8302,8304],{"class":178,"line":284},[176,8272,5225],{"class":224},[176,8274,8275],{"class":186}," `assets\u002F${",[176,8277,8278],{"class":228},"chunkInfo",[176,8280,408],{"class":186},[176,8282,8283],{"class":228},"name",[176,8285,408],{"class":186},[176,8287,3854],{"class":182},[176,8289,1237],{"class":186},[176,8291,3482],{"class":186},[176,8293,3469],{"class":224},[176,8295,8180],{"class":3472},[176,8297,3482],{"class":186},[176,8299,612],{"class":186},[176,8301,3871],{"class":186},[176,8303,4172],{"class":186},[176,8305,8306],{"class":186},"}-[hash].js`\n",[176,8308,8309],{"class":178,"line":298},[176,8310,8311],{"class":228},"      },\n",[176,8313,8314],{"class":178,"line":304},[176,8315,842],{"class":228},[176,8317,8318],{"class":178,"line":309},[176,8319,8320],{"class":228},"  },\n",[176,8322,8323],{"class":178,"line":350},[176,8324,8325],{"class":228},"},\n",[15,8327,8328,8329,8331],{},"这样就避免产生 ",[36,8330,8180],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[162,8333,8335],{"id":8334},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[15,8337,8338,8339,8342],{},"多次 ",[36,8340,8341],{},"go build"," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[15,8344,8345,8347,8348,8351],{},[25,8346,946],{},"：使用 ",[36,8349,8350],{},"go build -a"," 强制重新编译所有包。",[11,8353,8354],{"id":8354},"最终效果",[167,8356,8358],{"className":169,"code":8357,"language":171,"meta":172,"style":172},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[36,8359,8360,8370,8380],{"__ignoreMap":172},[176,8361,8362,8364,8367],{"class":178,"line":179},[176,8363,3479],{"class":182},[176,8365,8366],{"class":186}," bash",[176,8368,8369],{"class":186}," build-standalone.sh\n",[176,8371,8372,8374,8377],{"class":178,"line":244},[176,8373,3457],{"class":186},[176,8375,8376],{"class":186}," 构建成功",[176,8378,8379],{"class":186}," ===\n",[176,8381,8382,8385,8388],{"class":178,"line":251},[176,8383,8384],{"class":182},"输出:",[176,8386,8387],{"class":186}," service\u002Fmyapp.exe",[176,8389,8390],{"class":186},"  41M\n",[15,8392,8393],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[15,8395,8396],{},[25,8397,8398],{},"部署对比：",[48,8400,8401,8414],{},[51,8402,8403],{},[54,8404,8405,8408,8411],{},[57,8406,8407],{},"方式",[57,8409,8410],{},"文件数",[57,8412,8413],{},"操作",[70,8415,8416,8427],{},[54,8417,8418,8421,8424],{},[75,8419,8420],{},"旧方案",[75,8422,8423],{},"1 exe + 1 web 目录",[75,8425,8426],{},"解压→保持目录结构",[54,8428,8429,8432,8435],{},[75,8430,8431],{},"新方案",[75,8433,8434],{},"1 个 exe",[75,8436,8437],{},"直接扔到服务器运行",[11,8439,2534],{"id":2534},[15,8441,8442,8443,8445],{},"Go 的 ",[36,8444,7544],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[19,8447,8448,8455],{},[22,8449,8450,8451,1193,8453,8199],{},"嵌入目录时自动排除 ",[36,8452,8180],{},[36,8454,408],{},[22,8456,8457],{},"构建缓存可能导致 embed 不是最新的",[15,8459,8460],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[1346,8462,8463],{},"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":172,"searchDepth":244,"depth":251,"links":8465},[8466,8467,8468,8473,8479,8480],{"id":3237,"depth":244,"text":3237},{"id":7538,"depth":244,"text":7538},{"id":7552,"depth":244,"text":7552,"children":8469},[8470,8471,8472],{"id":7555,"depth":251,"text":7556},{"id":7892,"depth":251,"text":7893},{"id":8004,"depth":251,"text":8005},{"id":8142,"depth":244,"text":8142,"children":8474},[8475,8476,8478],{"id":8145,"depth":251,"text":8146},{"id":8173,"depth":251,"text":8477},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":8334,"depth":251,"text":8335},{"id":8354,"depth":244,"text":8354},{"id":2534,"depth":244,"text":2534},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fgo-embed-frontend","2026-06-06T16:00:00",{"title":7521,"description":8481},"posts\u002Fgo-embed-frontend\u002Findex",[2179,8489,8490,8491],"Vue","编译","埋坑","2026-06-06","SIQNL24wVAFb18wIGWSB3lNVZUQ9B76jWgQ55TASTYk",{"id":8495,"title":8496,"author":6,"body":8497,"description":9893,"difficulty":1367,"draft":1368,"extension":1369,"image":9894,"meta":9895,"navigation":262,"path":9896,"pinned":1368,"published":9897,"seo":9898,"series":6,"seriesOrder":6,"stem":9899,"tags":9900,"updated":9897,"__hash__":9902},"posts\u002Fposts\u002Fjira-cli-intro\u002Findex.md","jira_cli — 把企业 Jira 装进命令行，AI Agent 直接调用",{"type":8,"value":8498,"toc":9872},[8499,8503,8518,8585,8588,8591,8668,8670,8703,8710,8713,8717,8829,8833,8964,8968,9036,9039,9102,9105,9107,9111,9114,9134,9137,9152,9155,9159,9165,9303,9314,9345,9349,9352,9449,9453,9456,9574,9578,9581,9645,9648,9652,9655,9723,9726,9732,9735,9769,9776,9779,9839,9841,9844,9861,9869],[11,8500,8502],{"id":8501},"jira_cli-是什么","jira_cli 是什么",[15,8504,8505,8510,8511,1193,8514,8517],{},[3190,8506,8509],{"href":8507,"rel":8508},"https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli",[3194],"jira_cli"," 是一个面向企业 Jira（Server \u002F Data Center）的本地命令行工具，Go 语言编写，MIT 协议开源。它同时提供 ",[25,8512,8513],{},"Cobra CLI",[25,8515,8516],{},"MCP stdio server"," 两种入口，既能当日常命令行用，又能直接接入 AI 编程工具。",[167,8519,8521],{"className":169,"code":8520,"language":171,"meta":172,"style":172},"# 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",[36,8522,8523,8528,8541,8552,8567,8571,8576],{"__ignoreMap":172},[176,8524,8525],{"class":178,"line":179},[176,8526,8527],{"class":247},"# CLI 日常使用\n",[176,8529,8530,8532,8535,8538],{"class":178,"line":244},[176,8531,8509],{"class":182},[176,8533,8534],{"class":186}," issue",[176,8536,8537],{"class":186}," search",[176,8539,8540],{"class":186}," \"project = PROJ ORDER BY updated DESC\"\n",[176,8542,8543,8545,8547,8549],{"class":178,"line":251},[176,8544,8509],{"class":182},[176,8546,8534],{"class":186},[176,8548,4754],{"class":186},[176,8550,8551],{"class":186}," PROJ-123\n",[176,8553,8554,8556,8558,8561,8564],{"class":178,"line":259},[176,8555,8509],{"class":182},[176,8557,8534],{"class":186},[176,8559,8560],{"class":186}," comment",[176,8562,8563],{"class":186}," PROJ-123",[176,8565,8566],{"class":186}," \"处理完成，请测试验证\"\n",[176,8568,8569],{"class":178,"line":266},[176,8570,263],{"emptyLinePlaceholder":262},[176,8572,8573],{"class":178,"line":284},[176,8574,8575],{"class":247},"# MCP 模式 — 给 AI 装上操作 Jira 的手\n",[176,8577,8578,8580,8582],{"class":178,"line":298},[176,8579,8509],{"class":182},[176,8581,4993],{"class":186},[176,8583,8584],{"class":294}," --user=work\n",[11,8586,8587],{"id":8587},"为什么需要它",[15,8589,8590],{},"大多数 Jira 命令行工具只解决\"开发者手动操作 Jira\"这一个场景。但 2026 年 AI 编程工具已经普及，你的 AI Agent（Claude Code、Codex、Cursor 等）也需要读写 Jira。痛点就来了：",[48,8592,8593,8603],{},[51,8594,8595],{},[54,8596,8597,8600],{},[57,8598,8599],{},"痛点",[57,8601,8602],{},"jira_cli 怎么解",[70,8604,8605,8613,8621,8633,8645,8656],{},[54,8606,8607,8610],{},[75,8608,8609],{},"企业 Jira 在内网，没有公网 API",[75,8611,8612],{},"本地 stdio MCP server，不经过外网",[54,8614,8615,8618],{},[75,8616,8617],{},"AI 可能误删、误写 Jira 数据",[75,8619,8620],{},"账号级只读策略，AI 只能查不能改",[54,8622,8623,8626],{},[75,8624,8625],{},"团队多人多 Jira 实例",[75,8627,8628,8629,8632],{},"多 Profile 管理，",[36,8630,8631],{},"--user=work"," 切换",[54,8634,8635,8638],{},[75,8636,8637],{},"Windows 终端体验差",[75,8639,8640,8641,8644],{},"原生 ",[36,8642,8643],{},".exe"," 构建 + 一键安装脚本",[54,8646,8647,8650],{},[75,8648,8649],{},"AI 生成 JQL 困难",[75,8651,8652,8653],{},"字段元数据发现：",[36,8654,8655],{},"field list --query xxx",[54,8657,8658,8661],{},[75,8659,8660],{},"MCP 直接拿原始 JSON 太冗长",[75,8662,8663,8664,8667],{},"紧凑视图 + ",[36,8665,8666],{},"--view summary"," 减少 token 消耗",[11,8669,6885],{"id":6885},[167,8671,8673],{"className":169,"code":8672,"language":171,"meta":172,"style":172},"# 从源码安装（要求 Go 1.26.3+）\ngo install github.com\u002Fyourname\u002Fjira_cli@latest\n\n# 或下载预编译二进制\n# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[36,8674,8675,8680,8689,8693,8698],{"__ignoreMap":172},[176,8676,8677],{"class":178,"line":179},[176,8678,8679],{"class":247},"# 从源码安装（要求 Go 1.26.3+）\n",[176,8681,8682,8684,8686],{"class":178,"line":244},[176,8683,1522],{"class":182},[176,8685,187],{"class":186},[176,8687,8688],{"class":186}," github.com\u002Fyourname\u002Fjira_cli@latest\n",[176,8690,8691],{"class":178,"line":251},[176,8692,263],{"emptyLinePlaceholder":262},[176,8694,8695],{"class":178,"line":259},[176,8696,8697],{"class":247},"# 或下载预编译二进制\n",[176,8699,8700],{"class":178,"line":266},[176,8701,8702],{"class":247},"# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[15,8704,8705,8706,8709],{},"Windows 用户下载 zip 包解压后双击 ",[36,8707,8708],{},"install.bat"," 即可全局安装，自动加入 PATH。",[11,8711,8712],{"id":8712},"快速上手",[162,8714,8716],{"id":8715},"_1-配置认证","1. 配置认证",[167,8718,8720],{"className":169,"code":8719,"language":171,"meta":172,"style":172},"# 设置 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",[36,8721,8722,8727,8743,8747,8752,8772,8776,8781,8810,8814,8819],{"__ignoreMap":172},[176,8723,8724],{"class":178,"line":179},[176,8725,8726],{"class":247},"# 设置 Jira 地址\n",[176,8728,8729,8731,8734,8737,8740],{"class":178,"line":244},[176,8730,8509],{"class":182},[176,8732,8733],{"class":186}," set",[176,8735,8736],{"class":186}," base-url",[176,8738,8739],{"class":186}," https:\u002F\u002Fjira.your-company.com",[176,8741,8742],{"class":294}," --global\n",[176,8744,8745],{"class":178,"line":251},[176,8746,263],{"emptyLinePlaceholder":262},[176,8748,8749],{"class":178,"line":259},[176,8750,8751],{"class":247},"# Token 认证（推荐）\n",[176,8753,8754,8756,8758,8761,8763,8766,8768,8770],{"class":178,"line":266},[176,8755,8509],{"class":182},[176,8757,8733],{"class":186},[176,8759,8760],{"class":186}," token",[176,8762,1913],{"class":224},[176,8764,8765],{"class":186},"your-toke",[176,8767,2313],{"class":228},[176,8769,1301],{"class":224},[176,8771,8742],{"class":294},[176,8773,8774],{"class":178,"line":284},[176,8775,263],{"emptyLinePlaceholder":262},[176,8777,8778],{"class":178,"line":298},[176,8779,8780],{"class":247},"# 或用户名密码认证\n",[176,8782,8783,8785,8787,8789,8791,8794,8796,8798,8800,8803,8806,8808],{"class":178,"line":304},[176,8784,8509],{"class":182},[176,8786,8733],{"class":186},[176,8788,6958],{"class":186},[176,8790,1913],{"class":224},[176,8792,8793],{"class":186},"usernam",[176,8795,2301],{"class":228},[176,8797,1301],{"class":224},[176,8799,1913],{"class":224},[176,8801,8802],{"class":186},"passwor",[176,8804,8805],{"class":228},"d",[176,8807,1301],{"class":224},[176,8809,8742],{"class":294},[176,8811,8812],{"class":178,"line":309},[176,8813,263],{"emptyLinePlaceholder":262},[176,8815,8816],{"class":178,"line":350},[176,8817,8818],{"class":247},"# 验证一下\n",[176,8820,8821,8823,8826],{"class":178,"line":362},[176,8822,8509],{"class":182},[176,8824,8825],{"class":186}," user",[176,8827,8828],{"class":186}," me\n",[162,8830,8832],{"id":8831},"_2-日常使用","2. 日常使用",[167,8834,8836],{"className":169,"code":8835,"language":171,"meta":172,"style":172},"# 查自己今天的任务\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",[36,8837,8838,8843,8854,8858,8863,8873,8877,8882,8896,8913,8917,8922,8935,8939,8944],{"__ignoreMap":172},[176,8839,8840],{"class":178,"line":179},[176,8841,8842],{"class":247},"# 查自己今天的任务\n",[176,8844,8845,8847,8849,8851],{"class":178,"line":244},[176,8846,8509],{"class":182},[176,8848,8534],{"class":186},[176,8850,8537],{"class":186},[176,8852,8853],{"class":186}," \"assignee = currentUser() ORDER BY updated DESC\"\n",[176,8855,8856],{"class":178,"line":251},[176,8857,263],{"emptyLinePlaceholder":262},[176,8859,8860],{"class":178,"line":259},[176,8861,8862],{"class":247},"# 看某个任务详情\n",[176,8864,8865,8867,8869,8871],{"class":178,"line":266},[176,8866,8509],{"class":182},[176,8868,8534],{"class":186},[176,8870,4754],{"class":186},[176,8872,8551],{"class":186},[176,8874,8875],{"class":178,"line":284},[176,8876,263],{"emptyLinePlaceholder":262},[176,8878,8879],{"class":178,"line":298},[176,8880,8881],{"class":247},"# 流转状态\n",[176,8883,8884,8886,8888,8891,8893],{"class":178,"line":304},[176,8885,8509],{"class":182},[176,8887,8534],{"class":186},[176,8889,8890],{"class":186}," transitions",[176,8892,8563],{"class":186},[176,8894,8895],{"class":247},"                    # 可以转哪些\n",[176,8897,8898,8900,8902,8905,8907,8910],{"class":178,"line":309},[176,8899,8509],{"class":182},[176,8901,8534],{"class":186},[176,8903,8904],{"class":186}," do-transition",[176,8906,8563],{"class":186},[176,8908,8909],{"class":294}," 21",[176,8911,8912],{"class":247},"               # 执行流转\n",[176,8914,8915],{"class":178,"line":350},[176,8916,263],{"emptyLinePlaceholder":262},[176,8918,8919],{"class":178,"line":362},[176,8920,8921],{"class":247},"# 添加备注\n",[176,8923,8924,8926,8928,8930,8932],{"class":178,"line":383},[176,8925,8509],{"class":182},[176,8927,8534],{"class":186},[176,8929,8560],{"class":186},[176,8931,8563],{"class":186},[176,8933,8934],{"class":186}," \"已定位到问题，正在修复\"\n",[176,8936,8937],{"class":178,"line":389},[176,8938,263],{"emptyLinePlaceholder":262},[176,8940,8941],{"class":178,"line":395},[176,8942,8943],{"class":247},"# 创建任务\n",[176,8945,8946,8948,8950,8952,8955,8958,8961],{"class":178,"line":420},[176,8947,8509],{"class":182},[176,8949,8534],{"class":186},[176,8951,6845],{"class":186},[176,8953,8954],{"class":186}," PROJ",[176,8956,8957],{"class":186}," Task",[176,8959,8960],{"class":294}," --fields",[176,8962,8963],{"class":186}," @fields.json\n",[162,8965,8967],{"id":8966},"_3-配给-ai-使用","3. 配给 AI 使用",[167,8969,8971],{"className":169,"code":8970,"language":171,"meta":172,"style":172},"# 先配置一个只读账号给 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",[36,8972,8973,8978,8995,9018],{"__ignoreMap":172},[176,8974,8975],{"class":178,"line":179},[176,8976,8977],{"class":247},"# 先配置一个只读账号给 AI 用\n",[176,8979,8980,8982,8984,8986,8988,8990,8993],{"class":178,"line":244},[176,8981,8509],{"class":182},[176,8983,8733],{"class":186},[176,8985,8736],{"class":186},[176,8987,8739],{"class":186},[176,8989,1910],{"class":294},[176,8991,8992],{"class":186}," ai",[176,8994,8742],{"class":294},[176,8996,8997,8999,9001,9003,9005,9008,9010,9012,9014,9016],{"class":178,"line":251},[176,8998,8509],{"class":182},[176,9000,8733],{"class":186},[176,9002,8760],{"class":186},[176,9004,1913],{"class":224},[176,9006,9007],{"class":186},"toke",[176,9009,2313],{"class":228},[176,9011,1301],{"class":224},[176,9013,1910],{"class":294},[176,9015,8992],{"class":186},[176,9017,8742],{"class":294},[176,9019,9020,9022,9024,9027,9030,9032,9034],{"class":178,"line":259},[176,9021,8509],{"class":182},[176,9023,8733],{"class":186},[176,9025,9026],{"class":186}," readonly",[176,9028,9029],{"class":186}," on",[176,9031,1910],{"class":294},[176,9033,8992],{"class":186},[176,9035,8742],{"class":294},[15,9037,9038],{},"然后在 AI 工具的 MCP 配置里加上：",[167,9040,9042],{"className":6557,"code":9041,"language":6559,"meta":172,"style":172},"{\n  \"mcpServers\": {\n    \"jira\": {\n      \"command\": \"jira_cli\",\n      \"args\": [\"mcp\", \"--user=ai\"]\n    }\n  }\n}\n",[36,9043,9044,9048,9054,9061,9073,9090,9094,9098],{"__ignoreMap":172},[176,9045,9046],{"class":178,"line":179},[176,9047,6566],{"class":228},[176,9049,9050,9052],{"class":178,"line":244},[176,9051,6571],{"class":294},[176,9053,3288],{"class":228},[176,9055,9056,9059],{"class":178,"line":251},[176,9057,9058],{"class":294},"    \"jira\"",[176,9060,3288],{"class":228},[176,9062,9063,9066,9068,9071],{"class":178,"line":259},[176,9064,9065],{"class":294},"      \"command\"",[176,9067,3074],{"class":228},[176,9069,9070],{"class":186},"\"jira_cli\"",[176,9072,417],{"class":228},[176,9074,9075,9078,9080,9083,9085,9088],{"class":178,"line":266},[176,9076,9077],{"class":294},"      \"args\"",[176,9079,3105],{"class":228},[176,9081,9082],{"class":186},"\"mcp\"",[176,9084,612],{"class":228},[176,9086,9087],{"class":186},"\"--user=ai\"",[176,9089,1835],{"class":228},[176,9091,9092],{"class":178,"line":284},[176,9093,3329],{"class":228},[176,9095,9096],{"class":178,"line":298},[176,9097,3334],{"class":228},[176,9099,9100],{"class":178,"line":304},[176,9101,301],{"class":228},[15,9103,9104],{},"这样 AI 就能查 Jira 但不能修改，既有了上下文，又不用担心误操作。",[11,9106,7366],{"id":7366},[162,9108,9110],{"id":9109},"_1-账号级只读策略","1. 账号级只读策略",[15,9112,9113],{},"这是 jira_cli 最实用的设计。给 AI Agent 配置只读账号：",[167,9115,9117],{"className":169,"code":9116,"language":171,"meta":172,"style":172},"jira_cli set readonly on --name ai\n",[36,9118,9119],{"__ignoreMap":172},[176,9120,9121,9123,9125,9127,9129,9131],{"class":178,"line":179},[176,9122,8509],{"class":182},[176,9124,8733],{"class":186},[176,9126,9026],{"class":186},[176,9128,9029],{"class":186},[176,9130,1910],{"class":294},[176,9132,9133],{"class":186}," ai\n",[15,9135,9136],{},"只读模式下：",[859,9138,9139,9142,9145],{},[22,9140,9141],{},"CLI 写命令被隐藏和拒绝",[22,9143,9144],{},"MCP 的写入工具不会注册",[22,9146,9147,9148,9151],{},"环境变量 ",[36,9149,9150],{},"JIRA_READONLY=true"," 也可以临时覆盖",[15,9153,9154],{},"AI 只能查 JQL、看任务、查字段、读评论，绝对不会误改数据。需要创建\u002F更新时，另配一个有写入权限的账号由开发者自己操作。",[162,9156,9158],{"id":9157},"_2-多-profile-切换","2. 多 Profile 切换",[15,9160,9161,9162,9164],{},"一个配置文件里管理多个 Jira 账号，",[36,9163,2266],{}," 切换：",[167,9166,9168],{"className":169,"code":9167,"language":171,"meta":172,"style":172},"# 工作账号\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",[36,9169,9170,9175,9193,9216,9220,9225,9243,9266,9270,9275,9289],{"__ignoreMap":172},[176,9171,9172],{"class":178,"line":179},[176,9173,9174],{"class":247},"# 工作账号\n",[176,9176,9177,9179,9181,9183,9186,9188,9191],{"class":178,"line":244},[176,9178,8509],{"class":182},[176,9180,8733],{"class":186},[176,9182,8736],{"class":186},[176,9184,9185],{"class":186}," https:\u002F\u002Fjira.company.com",[176,9187,1910],{"class":294},[176,9189,9190],{"class":186}," work",[176,9192,8742],{"class":294},[176,9194,9195,9197,9199,9201,9203,9206,9208,9210,9212,9214],{"class":178,"line":251},[176,9196,8509],{"class":182},[176,9198,8733],{"class":186},[176,9200,8760],{"class":186},[176,9202,1913],{"class":224},[176,9204,9205],{"class":186},"work-toke",[176,9207,2313],{"class":228},[176,9209,1301],{"class":224},[176,9211,1910],{"class":294},[176,9213,9190],{"class":186},[176,9215,8742],{"class":294},[176,9217,9218],{"class":178,"line":259},[176,9219,263],{"emptyLinePlaceholder":262},[176,9221,9222],{"class":178,"line":266},[176,9223,9224],{"class":247},"# 个人账号\n",[176,9226,9227,9229,9231,9233,9236,9238,9241],{"class":178,"line":284},[176,9228,8509],{"class":182},[176,9230,8733],{"class":186},[176,9232,8736],{"class":186},[176,9234,9235],{"class":186}," https:\u002F\u002Fjira.personal.com",[176,9237,1910],{"class":294},[176,9239,9240],{"class":186}," personal",[176,9242,8742],{"class":294},[176,9244,9245,9247,9249,9251,9253,9256,9258,9260,9262,9264],{"class":178,"line":298},[176,9246,8509],{"class":182},[176,9248,8733],{"class":186},[176,9250,8760],{"class":186},[176,9252,1913],{"class":224},[176,9254,9255],{"class":186},"personal-toke",[176,9257,2313],{"class":228},[176,9259,1301],{"class":224},[176,9261,1910],{"class":294},[176,9263,9240],{"class":186},[176,9265,8742],{"class":294},[176,9267,9268],{"class":178,"line":304},[176,9269,263],{"emptyLinePlaceholder":262},[176,9271,9272],{"class":178,"line":309},[176,9273,9274],{"class":247},"# 使用时切换\n",[176,9276,9277,9279,9282,9284,9286],{"class":178,"line":350},[176,9278,8509],{"class":182},[176,9280,9281],{"class":294}," --user=work",[176,9283,8534],{"class":186},[176,9285,8537],{"class":186},[176,9287,9288],{"class":186}," \"project = PROJ\"\n",[176,9290,9291,9293,9296,9298,9300],{"class":178,"line":362},[176,9292,8509],{"class":182},[176,9294,9295],{"class":294}," --user=personal",[176,9297,8534],{"class":186},[176,9299,8537],{"class":186},[176,9301,9302],{"class":186}," \"project = SIDE\"\n",[15,9304,9305,9306,9309,9310,9313],{},"同时支持",[25,9307,9308],{},"项目级配置","（",[36,9311,9312],{},"--local","）和工作目录绑定：",[167,9315,9317],{"className":169,"code":9316,"language":171,"meta":172,"style":172},"# 在项目目录下，配置只对当前项目生效\ncd ~\u002Fprojects\u002Fbackend\njira_cli set base-url https:\u002F\u002Fjira-backend.company.com --local\n",[36,9318,9319,9324,9331],{"__ignoreMap":172},[176,9320,9321],{"class":178,"line":179},[176,9322,9323],{"class":247},"# 在项目目录下，配置只对当前项目生效\n",[176,9325,9326,9328],{"class":178,"line":244},[176,9327,8108],{"class":294},[176,9329,9330],{"class":186}," ~\u002Fprojects\u002Fbackend\n",[176,9332,9333,9335,9337,9339,9342],{"class":178,"line":251},[176,9334,8509],{"class":182},[176,9336,8733],{"class":186},[176,9338,8736],{"class":186},[176,9340,9341],{"class":186}," https:\u002F\u002Fjira-backend.company.com",[176,9343,9344],{"class":294}," --local\n",[162,9346,9348],{"id":9347},"_3-对-ai-友好的输出","3. 对 AI 友好的输出",[15,9350,9351],{},"jira_cli 在输出设计上充分考虑了 AI Agent 的 token 消耗：",[167,9353,9355],{"className":169,"code":9354,"language":171,"meta":172,"style":172},"# 紧凑视图 — 去掉 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",[36,9356,9357,9362,9378,9382,9387,9404,9408,9413,9428,9432,9437],{"__ignoreMap":172},[176,9358,9359],{"class":178,"line":179},[176,9360,9361],{"class":247},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\n",[176,9363,9364,9366,9368,9370,9372,9375],{"class":178,"line":244},[176,9365,8509],{"class":182},[176,9367,8534],{"class":186},[176,9369,4754],{"class":186},[176,9371,8563],{"class":186},[176,9373,9374],{"class":294}," --view",[176,9376,9377],{"class":186}," summary\n",[176,9379,9380],{"class":178,"line":251},[176,9381,263],{"emptyLinePlaceholder":262},[176,9383,9384],{"class":178,"line":259},[176,9385,9386],{"class":247},"# 表格输出 — 一眼看到关键信息\n",[176,9388,9389,9391,9393,9395,9398,9401],{"class":178,"line":266},[176,9390,8509],{"class":182},[176,9392,8534],{"class":186},[176,9394,8537],{"class":186},[176,9396,9397],{"class":186}," \"project = PROJ\"",[176,9399,9400],{"class":294}," --output",[176,9402,9403],{"class":186}," table\n",[176,9405,9406],{"class":178,"line":284},[176,9407,263],{"emptyLinePlaceholder":262},[176,9409,9410],{"class":178,"line":298},[176,9411,9412],{"class":247},"# JSON 输出 — 程序解析，不转义 Unicode\n",[176,9414,9415,9417,9419,9422,9424,9426],{"class":178,"line":304},[176,9416,8509],{"class":182},[176,9418,9400],{"class":294},[176,9420,9421],{"class":186}," json",[176,9423,8534],{"class":186},[176,9425,4754],{"class":186},[176,9427,8551],{"class":186},[176,9429,9430],{"class":178,"line":309},[176,9431,263],{"emptyLinePlaceholder":262},[176,9433,9434],{"class":178,"line":350},[176,9435,9436],{"class":247},"# 自定义搜索列 — 只要需要的字段\n",[176,9438,9439,9441,9443,9446],{"class":178,"line":362},[176,9440,8509],{"class":182},[176,9442,8733],{"class":186},[176,9444,9445],{"class":186}," search-columns",[176,9447,9448],{"class":186}," key,summary,fixVersions\n",[162,9450,9452],{"id":9451},"_4-字段元数据发现","4. 字段元数据发现",[15,9454,9455],{},"Jira 的 customfield 让人头疼，jira_cli 提供了完整的字段发现能力：",[167,9457,9459],{"className":169,"code":9458,"language":171,"meta":172,"style":172},"# 列出所有字段及其类型\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",[36,9460,9461,9466,9475,9479,9484,9498,9502,9507,9518,9522,9527,9544,9548,9553],{"__ignoreMap":172},[176,9462,9463],{"class":178,"line":179},[176,9464,9465],{"class":247},"# 列出所有字段及其类型\n",[176,9467,9468,9470,9473],{"class":178,"line":244},[176,9469,8509],{"class":182},[176,9471,9472],{"class":186}," field",[176,9474,3418],{"class":186},[176,9476,9477],{"class":178,"line":251},[176,9478,263],{"emptyLinePlaceholder":262},[176,9480,9481],{"class":178,"line":259},[176,9482,9483],{"class":247},"# 搜索包含特定关键词的字段\n",[176,9485,9486,9488,9490,9492,9495],{"class":178,"line":266},[176,9487,8509],{"class":182},[176,9489,9472],{"class":186},[176,9491,6780],{"class":186},[176,9493,9494],{"class":294}," --query",[176,9496,9497],{"class":186}," \"版本\"\n",[176,9499,9500],{"class":178,"line":284},[176,9501,263],{"emptyLinePlaceholder":262},[176,9503,9504],{"class":178,"line":298},[176,9505,9506],{"class":247},"# 只看自定义字段\n",[176,9508,9509,9511,9513,9515],{"class":178,"line":304},[176,9510,8509],{"class":182},[176,9512,9472],{"class":186},[176,9514,6780],{"class":186},[176,9516,9517],{"class":294}," --custom-only\n",[176,9519,9520],{"class":178,"line":309},[176,9521,263],{"emptyLinePlaceholder":262},[176,9523,9524],{"class":178,"line":350},[176,9525,9526],{"class":247},"# 创建任务前，先看看需要哪些字段\n",[176,9528,9529,9531,9533,9536,9538,9541],{"class":178,"line":362},[176,9530,8509],{"class":182},[176,9532,8534],{"class":186},[176,9534,9535],{"class":186}," create-meta",[176,9537,8954],{"class":186},[176,9539,9540],{"class":294}," --issue-type",[176,9542,9543],{"class":186}," Task\n",[176,9545,9546],{"class":178,"line":383},[176,9547,263],{"emptyLinePlaceholder":262},[176,9549,9550],{"class":178,"line":389},[176,9551,9552],{"class":247},"# 分页查看可选值\n",[176,9554,9555,9557,9559,9562,9564,9566,9569,9571],{"class":178,"line":395},[176,9556,8509],{"class":182},[176,9558,8534],{"class":186},[176,9560,9561],{"class":186}," create-meta-values",[176,9563,8954],{"class":186},[176,9565,8957],{"class":186},[176,9567,9568],{"class":186}," priority",[176,9570,9494],{"class":294},[176,9572,9573],{"class":186}," High\n",[162,9575,9577],{"id":9576},"_5-mcp-写入控制","5. MCP 写入控制",[15,9579,9580],{},"除了全局只读，MCP 还有更细粒度的控制：",[48,9582,9583,9593],{},[51,9584,9585],{},[54,9586,9587,9590],{},[57,9588,9589],{},"环境变量",[57,9591,9592],{},"作用",[70,9594,9595,9605,9615,9625,9635],{},[54,9596,9597,9602],{},[75,9598,9599],{},[36,9600,9601],{},"JIRA_CREATE_OPEN=false",[75,9603,9604],{},"禁用创建\u002F更新工具",[54,9606,9607,9612],{},[75,9608,9609],{},[36,9610,9611],{},"JIRA_DISABLE_DISCLAIMER=true",[75,9613,9614],{},"去掉 AI 自动添加的备注声明",[54,9616,9617,9622],{},[75,9618,9619],{},[36,9620,9621],{},"JIRA_CREATE_SUMMARY_MAX",[75,9623,9624],{},"限制摘要长度",[54,9626,9627,9632],{},[75,9628,9629],{},[36,9630,9631],{},"JIRA_CREATE_DESCRIPTION_MAX",[75,9633,9634],{},"限制描述长度",[54,9636,9637,9642],{},[75,9638,9639],{},[36,9640,9641],{},"JIRA_CREATE_FIELDS_MAX_BYTES",[75,9643,9644],{},"限制创建字段的 JSON 大小",[15,9646,9647],{},"这些限制确保 AI 即使有写入权限也不会\"失控\"——例如防止 AI 往描述里塞几万字的废话。",[162,9649,9651],{"id":9650},"_6-中英双语帮助","6. 中英双语帮助",[15,9653,9654],{},"帮助信息默认中文，也支持一键切英文：",[167,9656,9658],{"className":169,"code":9657,"language":171,"meta":172,"style":172},"# 保存英文偏好\njira_cli set help-language en\n\n# 切回中文\njira_cli set help-language zh\n\n# 临时覆盖\njira_cli --lang en issue search -h\n",[36,9659,9660,9665,9677,9681,9686,9697,9701,9706],{"__ignoreMap":172},[176,9661,9662],{"class":178,"line":179},[176,9663,9664],{"class":247},"# 保存英文偏好\n",[176,9666,9667,9669,9671,9674],{"class":178,"line":244},[176,9668,8509],{"class":182},[176,9670,8733],{"class":186},[176,9672,9673],{"class":186}," help-language",[176,9675,9676],{"class":186}," en\n",[176,9678,9679],{"class":178,"line":251},[176,9680,263],{"emptyLinePlaceholder":262},[176,9682,9683],{"class":178,"line":259},[176,9684,9685],{"class":247},"# 切回中文\n",[176,9687,9688,9690,9692,9694],{"class":178,"line":266},[176,9689,8509],{"class":182},[176,9691,8733],{"class":186},[176,9693,9673],{"class":186},[176,9695,9696],{"class":186}," zh\n",[176,9698,9699],{"class":178,"line":284},[176,9700,263],{"emptyLinePlaceholder":262},[176,9702,9703],{"class":178,"line":298},[176,9704,9705],{"class":247},"# 临时覆盖\n",[176,9707,9708,9710,9713,9716,9718,9720],{"class":178,"line":304},[176,9709,8509],{"class":182},[176,9711,9712],{"class":294}," --lang",[176,9714,9715],{"class":186}," en",[176,9717,8534],{"class":186},[176,9719,8537],{"class":186},[176,9721,9722],{"class":294}," -h\n",[11,9724,9725],{"id":9725},"架构一览",[167,9727,9730],{"className":9728,"code":9729,"language":2692},[2690],"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",[36,9731,9729],{"__ignoreMap":172},[15,9733,9734],{},"设计原则清晰：",[859,9736,9737,9745,9753,9761],{},[22,9738,9739,9744],{},[25,9740,9741],{},[36,9742,9743],{},"pkg\u002Fjira"," 是纯 Jira 客户端，HTTP、认证、上下文处理都在这里",[22,9746,9747,9752],{},[25,9748,9749],{},[36,9750,9751],{},"cmd"," 只管 Cobra 解析和呈现",[22,9754,9755,9760],{},[25,9756,9757],{},[36,9758,9759],{},"internal\u002Ftools\u002Fjira_tools"," 只管 MCP schema 验证和结果适配",[22,9762,9763,9768],{},[25,9764,9765],{},[36,9766,9767],{},"internal\u002Fconfig"," 拥有 Profile 的全部逻辑，不分散到 cmd",[15,9770,9771,9772,9775],{},"每一层职责单一，新增 Jira 能力时按 ",[36,9773,9774],{},"endpoints.go → Client 方法 → CLI 命令 → MCP tool → 策略注册"," 的路径走就行。",[11,9777,9778],{"id":9778},"和已有方案的对比",[48,9780,9781,9793],{},[51,9782,9783],{},[54,9784,9785,9787,9790],{},[57,9786,59],{},[57,9788,9789],{},"适合场景",[57,9791,9792],{},"jira_cli 的区别",[70,9794,9795,9806,9817,9828],{},[54,9796,9797,9800,9803],{},[75,9798,9799],{},"Atlassian Rovo MCP Server",[75,9801,9802],{},"Jira Cloud、官方 OAuth",[75,9804,9805],{},"jira_cli 是本地 stdio，适合内网 Server\u002FDC",[54,9807,9808,9811,9814],{},[75,9809,9810],{},"go-jira \u002F jira-cli 社区版",[75,9812,9813],{},"纯命令行管理",[75,9815,9816],{},"jira_cli 同时提供 MCP server，面向 AI 工作流",[54,9818,9819,9822,9825],{},[75,9820,9821],{},"直接调 REST API",[75,9823,9824],{},"自定义集成",[75,9826,9827],{},"jira_cli 封装了认证、只读策略、字段元数据",[54,9829,9830,9833,9836],{},[75,9831,9832],{},"Jira 网页",[75,9834,9835],{},"日常使用",[75,9837,9838],{},"jira_cli 把 Jira 变成终端里的一行命令",[11,9840,2534],{"id":2534},[15,9842,9843],{},"如果你的团队满足以下任一条件，jira_cli 值得一试：",[859,9845,9846,9849,9852,9855,9858],{},[22,9847,9848],{},"企业 Jira 部署在内网，想接入 AI 编程工具",[22,9850,9851],{},"想给 AI Agent 只读权限来查 Jira 上下文",[22,9853,9854],{},"需要命令行快速查看\u002F操作 Jira，不想切浏览器",[22,9856,9857],{},"需要管理多个 Jira 实例和多套账号",[22,9859,9860],{},"Windows 用户想要无摩擦的 CLI 体验",[15,9862,9863,9864,9868],{},"GitHub：",[3190,9865,9867],{"href":8507,"rel":9866},[3194],"github.com\u002Fyourname\u002Fjira_cli"," | MIT 协议 | Go 1.26.3+",[1346,9870,9871],{},"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":172,"searchDepth":244,"depth":251,"links":9873},[9874,9875,9876,9877,9882,9890,9891,9892],{"id":8501,"depth":244,"text":8502},{"id":8587,"depth":244,"text":8587},{"id":6885,"depth":244,"text":6885},{"id":8712,"depth":244,"text":8712,"children":9878},[9879,9880,9881],{"id":8715,"depth":251,"text":8716},{"id":8831,"depth":251,"text":8832},{"id":8966,"depth":251,"text":8967},{"id":7366,"depth":244,"text":7366,"children":9883},[9884,9885,9886,9887,9888,9889],{"id":9109,"depth":251,"text":9110},{"id":9157,"depth":251,"text":9158},{"id":9347,"depth":251,"text":9348},{"id":9451,"depth":251,"text":9452},{"id":9576,"depth":251,"text":9577},{"id":9650,"depth":251,"text":9651},{"id":9725,"depth":244,"text":9725},{"id":9778,"depth":244,"text":9778},{"id":2534,"depth":244,"text":2534},"介绍 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":8496,"description":9893},"posts\u002Fjira-cli-intro\u002Findex",[9901,7516,6732,2179,6734,7517],"Jira","vNH6sF2mMFmSjbHoEI2OqmYCgpFmaqyKt-MUfcQqXrU",{"id":9904,"title":9905,"author":6,"body":9906,"description":10450,"difficulty":1367,"draft":1368,"extension":1369,"image":10451,"meta":10452,"navigation":262,"path":10453,"pinned":1368,"published":10454,"seo":10455,"series":10456,"seriesOrder":179,"stem":10457,"tags":10458,"updated":10460,"__hash__":10461},"posts\u002Fposts\u002Fsublog-evolution\u002Findex.md","尼克的小窝 架构变迁：从纯静态到前后端分离",{"type":8,"value":9907,"toc":10430},[9908,9912,9915,9926,9930,9934,9940,9944,9975,9978,9992,9996,9999,10067,10071,10074,10080,10084,10094,10124,10127,10131,10139,10142,10186,10190,10311,10315,10322,10326,10332,10338,10342,10345,10396,10400,10407,10414,10420,10423,10425],[11,9909,9911],{"id":9910},"写在前面","📝 写在前面",[15,9913,9914],{},"SuBlog 最初只是一个简单的技术博客——Nuxt 3 静态生成，Markdown 写文章，部署到静态托管完事。但随着功能需求的增长，纯静态的架构开始显露出局限性。",[15,9916,9917,9918,9921,9922,9925],{},"这篇文章记录从 ",[25,9919,9920],{},"纯静态"," 到 ",[25,9923,9924],{},"前后端分离"," 的演进过程，带你了解每一步背后的思考。",[11,9927,9929],{"id":9928},"第一代纯静态nuxt-3-nuxtcontent","📦 第一代：纯静态（Nuxt 3 + @nuxt\u002Fcontent）",[162,9931,9933],{"id":9932},"️-架构图","🏗️ 架构图",[167,9935,9938],{"className":9936,"code":9937,"language":2692,"meta":172},[2690],"┌─────────────────────────────────────┐\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",[36,9939,9937],{"__ignoreMap":172},[162,9941,9943],{"id":9942},"做了什么","🔧 做了什么",[859,9945,9946,9955,9960,9965],{},[22,9947,9948,2551,9950,2551,9952,9954],{},[25,9949,2861],{},[25,9951,2871],{},[25,9953,2881],{}," 搭建前端",[22,9956,9957,9959],{},[25,9958,2891],{}," 管理 Markdown 文章，编译时生成静态页面",[22,9961,9962,9964],{},[25,9963,2911],{}," 嵌入评论，依赖 GitHub Discussions",[22,9966,9967,9968,9971,9972,9974],{},"部署通过 ",[36,9969,9970],{},"nuxt generate"," 输出 ",[36,9973,3147],{},"，丢到 Nginx 或 Vercel",[162,9976,9977],{"id":4033},"✅ 优点",[859,9979,9980,9986,9989],{},[22,9981,9982,9983,9985],{},"部署极简，一个 ",[36,9984,9970],{}," 搞定",[22,9987,9988],{},"加载飞快，全是 HTML，没有服务端开销",[22,9990,9991],{},"无需后端，零运维成本",[162,9993,9995],{"id":9994},"局限","❌ 局限",[15,9997,9998],{},"随着时间的推移，纯静态的问题越来越明显：",[48,10000,10001,10010],{},[51,10002,10003],{},[54,10004,10005,10007],{},[57,10006,4327],{},[57,10008,10009],{},"具体表现",[70,10011,10012,10023,10034,10045,10056],{},[54,10013,10014,10020],{},[75,10015,10016,10017],{},"🐢 ",[25,10018,10019],{},"内容更新慢",[75,10021,10022],{},"改一篇文章要重新构建、重新部署，不能实时发布",[54,10024,10025,10031],{},[75,10026,10027,10028],{},"👤 ",[25,10029,10030],{},"没有用户体系",[75,10032,10033],{},"无法区分读者和管理员，所有内容对所有人一致可见",[54,10035,10036,10042],{},[75,10037,10038,10039],{},"🔒 ",[25,10040,10041],{},"评论受限",[75,10043,10044],{},"Giscus 需要 GitHub 账号才能评论，门槛高",[54,10046,10047,10053],{},[75,10048,10049,10050],{},"🚫 ",[25,10051,10052],{},"无法做动态功能",[75,10054,10055],{},"点赞、收藏、阅读统计、个性化推荐……统统做不了",[54,10057,10058,10064],{},[75,10059,10060,10061],{},"🔗 ",[25,10062,10063],{},"前端锁死",[75,10065,10066],{},"被 Nuxt\u002FVue 生态绑定，想换技术栈得重写",[11,10068,10070],{"id":10069},"第二代前后端分离react-go","⚡ 第二代：前后端分离（React + Go）",[162,10072,9933],{"id":10073},"️-架构图-1",[167,10075,10078],{"className":10076,"code":10077,"language":2692,"meta":172},[2690],"┌───────────────────────┐       ┌────────────────────┐\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",[36,10079,10077],{"__ignoreMap":172},[162,10081,10083],{"id":10082},"️-前端sublog-react","🖥️ 前端：sublog-react",[15,10085,10086,10087,10089,10090,10093],{},"选用 ",[25,10088,2719],{},"（App Router） + ",[25,10091,10092],{},"React 19","，理由是：",[859,10095,10096,10103,10110,10117],{},[22,10097,10098,10099,10102],{},"🎨 ",[25,10100,10101],{},"React 生态","——组件库成熟，开发效率高",[22,10104,10105,10106,10109],{},"📐 ",[25,10107,10108],{},"App Router","——支持布局嵌套，页面组织清晰",[22,10111,10112,10113,10116],{},"📦 ",[25,10114,10115],{},"静态导出","——仍可部署到静态托管",[22,10118,10119,10120,10123],{},"⚙️ ",[25,10121,10122],{},"运行时配置","——API 地址可动态注入，切换环境无需重新构建",[15,10125,10126],{},"前端不再直接读取 Markdown 文件，而是通过 HTTP API 获取文章数据。静态导出 + 客户端数据获取的组合，既保留了首屏加载速度，又实现了内容的动态更新。",[162,10128,10130],{"id":10129},"️-后端sublog-s","⚙️ 后端：sublog-s",[15,10132,10133,10134,2551,10136,10138],{},"后端使用 ",[25,10135,2179],{},[25,10137,6733],{},"，按业务逻辑分层组织代码。",[15,10140,10141],{},"核心技术选型：",[48,10143,10144,10152],{},[51,10145,10146],{},[54,10147,10148,10150],{},[57,10149,2849],{},[57,10151,2852],{},[70,10153,10154,10164,10175],{},[54,10155,10156,10161],{},[75,10157,10158,10159],{},"🚀 ",[25,10160,6733],{},[75,10162,10163],{},"HTTP 框架",[54,10165,10166,10172],{},[75,10167,10168,10169],{},"🗄️ ",[25,10170,10171],{},"GORM",[75,10173,10174],{},"ORM，数据库操作",[54,10176,10177,10183],{},[75,10178,10179,10180],{},"🔐 ",[25,10181,10182],{},"JWT",[75,10184,10185],{},"用户认证",[11,10187,10189],{"id":10188},"前后对比","📊 前后对比",[48,10191,10192,10205],{},[51,10193,10194],{},[54,10195,10196,10199,10202],{},[57,10197,10198],{},"维度",[57,10200,10201],{},"第一代（纯静态）",[57,10203,10204],{},"第二代（前后端分离）",[70,10206,10207,10219,10231,10245,10258,10271,10285,10297],{},[54,10208,10209,10213,10216],{},[75,10210,10098,10211],{},[25,10212,2814],{},[75,10214,10215],{},"Nuxt 3 (Vue)",[75,10217,10218],{},"Next.js (React 19)",[54,10220,10221,10226,10228],{},[75,10222,10223,10224],{},"🖥️ ",[25,10225,6735],{},[75,10227,84],{},[75,10229,10230],{},"Go + Gin",[54,10232,10233,10239,10242],{},[75,10234,10235,10236],{},"📄 ",[25,10237,10238],{},"内容来源",[75,10240,10241],{},"Markdown 文件",[75,10243,10244],{},"API 数据库",[54,10246,10247,10252,10255],{},[75,10248,10158,10249],{},[25,10250,10251],{},"发布方式",[75,10253,10254],{},"全量重新构建",[75,10256,10257],{},"API 实时发布",[54,10259,10260,10265,10268],{},[75,10261,10027,10262],{},[25,10263,10264],{},"用户系统",[75,10266,10267],{},"❌ 无",[75,10269,10270],{},"✅ 注册\u002F登录",[54,10272,10273,10279,10282],{},[75,10274,10275,10276],{},"💬 ",[25,10277,10278],{},"评论",[75,10280,10281],{},"Giscus（需 GitHub）",[75,10283,10284],{},"自有评论系统（规划中）",[54,10286,10287,10291,10294],{},[75,10288,10112,10289],{},[25,10290,3122],{},[75,10292,10293],{},"1 个静态目录",[75,10295,10296],{},"前端静态 + 后端二进制",[54,10298,10299,10305,10308],{},[75,10300,10301,10302],{},"⚡ ",[25,10303,10304],{},"更新体验",[75,10306,10307],{},"改文章 → 构建 → 部署",[75,10309,10310],{},"改文章 → 立即生效",[11,10312,10314],{"id":10313},"新功能注册账号","🎉 新功能：注册账号",[15,10316,10317,10318,10321],{},"伴随着架构升级，",[25,10319,10320],{},"用户账号系统","也上线了。",[162,10323,10325],{"id":10324},"目前支持","📋 目前支持",[15,10327,10328,10329,1032],{},"现在你可以在尼克的小窝 ",[25,10330,10331],{},"注册一个账号",[167,10333,10336],{"className":10334,"code":10335,"language":2692},[2690],"1. ✏️ 填写用户名、邮箱、密码\n2. 📧 查收激活邮件\n3. 🔗 点击链接激活账号\n4. 🚪 登录后查看和管理个人信息\n",[36,10337,10335],{"__ignoreMap":172},[11,10339,10341],{"id":10340},"️-后续计划","🗓️ 后续计划",[15,10343,10344],{},"正在开发或规划中的功能：",[859,10346,10348,10354,10360,10366,10372,10378,10384,10390],{"className":10347},[3155],[22,10349,10351,10353],{"className":10350},[3159],[3161,10352],{"checked":262,"disabled":262,"type":3163}," 用户注册\u002F登录（已上线 🎉）",[22,10355,10357,10359],{"className":10356},[3159],[3161,10358],{"checked":262,"disabled":262,"type":3163}," 个人资料编辑（已上线 🎉）",[22,10361,10363,10365],{"className":10362},[3159],[3161,10364],{"disabled":262,"type":3163}," 💬 站内评论系统",[22,10367,10369,10371],{"className":10368},[3159],[3161,10370],{"disabled":262,"type":3163}," 🔖 文章收藏",[22,10373,10375,10377],{"className":10374},[3159],[3161,10376],{"disabled":262,"type":3163}," 📚 阅读历史",[22,10379,10381,10383],{"className":10380},[3159],[3161,10382],{"disabled":262,"type":3163}," 👍 文章点赞",[22,10385,10387,10389],{"className":10386},[3159],[3161,10388],{"disabled":262,"type":3163}," 🔔 关注通知",[22,10391,10393,10395],{"className":10392},[3159],[3161,10394],{"disabled":262,"type":3163}," 🎨 个人首页定制",[11,10397,10399],{"id":10398},"结语","💡 结语",[15,10401,10402,10403,10406],{},"从 Nuxt 3 纯静态到 React + Go 前后端分离，SuBlog 的架构变迁本质上是一个",[25,10404,10405],{},"功能驱动","的过程。静态博客有它的位置——写写文章、展示内容完全够用。但当你想做的不仅仅是\"展示\"时，后端的能力就不可或缺了。",[15,10408,10409,10410,10413],{},"这次重构最大的感触是：",[25,10411,10412],{},"架构选型不是越复杂越好，而是刚好够用最好","。纯静态阶段用 Nuxt 3 一点问题没有，直到功能需求超出了静态的边界，才自然推动到前后端分离。",[15,10415,10416,10417],{},"如果你也在纠结\"要不要上后端\"，不妨先问问自己：",[25,10418,10419],{},"你要做的是\"展示内容\"还是\"提供服务\"？",[15,10421,10422],{},"前者，静态就够了；后者，来吧，后端欢迎你。😄",[3198,10424],{},[15,10426,10427],{},[25,10428,10429],{},"🎉 目前注册功能已经可以用了，后续更多好玩的功能正在路上。部分功能可能需要登录才能体验，快注册一个账号吧！",{"title":172,"searchDepth":244,"depth":251,"links":10431},[10432,10433,10439,10444,10445,10448,10449],{"id":9910,"depth":244,"text":9911},{"id":9928,"depth":244,"text":9929,"children":10434},[10435,10436,10437,10438],{"id":9932,"depth":251,"text":9933},{"id":9942,"depth":251,"text":9943},{"id":4033,"depth":251,"text":9977},{"id":9994,"depth":251,"text":9995},{"id":10069,"depth":244,"text":10070,"children":10440},[10441,10442,10443],{"id":10073,"depth":251,"text":9933},{"id":10082,"depth":251,"text":10083},{"id":10129,"depth":251,"text":10130},{"id":10188,"depth":244,"text":10189},{"id":10313,"depth":244,"text":10314,"children":10446},[10447],{"id":10324,"depth":251,"text":10325},{"id":10340,"depth":244,"text":10341},{"id":10398,"depth":244,"text":10399},"记录尼克的小窝从 Nuxt 3 纯静态博客演进到 React + Go 前后端分离架构的全过程，包括架构设计的思考、技术选型的变化，以及新增的账号注册功能。","\u002Fposts\u002Fsublog-evolution\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsublog-evolution","2026-07-23T12:00:00",{"title":9905,"description":10450},"尼克的小窝 开发记录","posts\u002Fsublog-evolution\u002Findex",[10459,1378,2719,2179],"架构","2026-07-23","VMQ6A2K8GQ5t3DSlezWPd3KgXnmDVMn9QVeaI28Hcp8",{"id":10463,"title":10464,"author":6,"body":10465,"description":11627,"difficulty":1367,"draft":1368,"extension":1369,"image":11628,"meta":11629,"navigation":262,"path":11630,"pinned":1368,"published":11631,"seo":11632,"series":6,"seriesOrder":6,"stem":11633,"tags":11634,"updated":1382,"__hash__":11638},"posts\u002Fposts\u002Fip-geolocation-guide\u002Findex.md","IP 属地是怎么查出来的？本地查询与在线查询方案对比",{"type":8,"value":10466,"toc":11603},[10467,10469,10475,10478,10489,10499,10503,10509,10522,10528,10539,10546,10549,10556,10560,10571,10575,10586,10589,10593,10599,10605,10608,10612,10619,10637,10640,10643,10647,10678,10717,10723,10727,10866,10902,10911,10915,10955,10960,10964,10975,10978,11012,11015,11019,11022,11026,11217,11221,11304,11308,11315,11422,11426,11435,11521,11524,11527,11563,11566,11568,11576,11597,11600],[11,10468,9910],{"id":9910},[15,10470,10471,10472],{},"很多网站会在「登录记录」「安全中心」里显示你的登录 IP 归属地，比如「福建·福州 · 中国移动」。你可能好奇过：",[25,10473,10474],{},"服务器是怎么根据一个 IP 就判断出我在哪个城市、哪个运营商的？",[15,10476,10477],{},"这篇科普文章讲清楚三件事：",[19,10479,10480,10483,10486],{},[22,10481,10482],{},"IP 属地到底查的是什么；",[22,10484,10485],{},"纯本地查询为什么能做到毫秒级且不联网；",[22,10487,10488],{},"几种主流查询方案的优缺点对比，方便你按场景选型。",[10490,10491,10492],"blockquote",{},[15,10493,10494,10495,10498],{},"说明：文中用到的 IP 均为 RFC 5737 保留的文档示例段（TEST-NET-2，",[36,10496,10497],{},"198.51.100.0\u002F24","），不会对应任何真实地址。",[11,10500,10502],{"id":10501},"先想清楚ip-属地查的是什么","先想清楚：IP 属地查的是什么",[15,10504,10505,10506,910],{},"一个常见误区是「每个 IP 对应一个具体地址」。实际上 IP 属地的本质是 ",[25,10507,10508],{},"IP 段与归属地的映射",[15,10510,10511,10512,1032],{},"运营商和机构在申请 IP 时，是按",[25,10513,10514,10515,10521],{},"网段（CIDR）",[25,10516,10517,10518,10520],{},"成块分配的。比如 ",[36,10519,10497],{}," 这一个段，一整块都归属同一地区、同一运营商。所以归属地数据天然可以","按段存储",[167,10523,10526],{"className":10524,"code":10525,"language":2692,"meta":172},[2690],"IP 起始          IP 结束         归属地\n────────────────────────────────────────\n198.51.100.0     198.51.100.255  中国|福建|福州|移动\n198.51.101.0     198.51.101.255  中国|浙江|杭州|电信\n",[36,10527,10525],{"__ignoreMap":172},[15,10529,10530,10531,10534,10535,10538],{},"全球公网 IP 约有 42 亿个，但",[25,10532,10533],{},"网段只有几十万条","。正因为存的是「段」而不是「每个 IP」，完整的归属地数据文件才只有几 MB（ip2region 的 ",[36,10536,10537],{},".xdb"," 约 11MB）。这是「本地离线查询」可行的第一前提。",[15,10540,10541,10542,10545],{},"所以，",[25,10543,10544],{},"IP 属地从来不是精确到街道的定位","，它返回的是段级的「省｜市｜运营商」粗粒度信息。",[11,10547,10548],{"id":10548},"纯本地查询的原理",[15,10550,10551,10552,10555],{},"以开源的 ",[25,10553,10554],{},"ip2region"," 为例，它的查询完全在本地完成，不联网、不请求第三方，几十微秒到毫秒级返回。",[162,10557,10559],{"id":10558},"第一步把-ip-变成整数","第一步：把 IP 变成整数",[15,10561,10562,10563,10566,10567,10570],{},"以 ",[36,10564,10565],{},"198.51.100.42"," 为例，它会转成一个 32 位整数 ",[36,10568,10569],{},"0xC633642A","。IP 的四个数字本质就是 4 个字节，拼起来就是一个整数，这一步没有任何玄学。",[162,10572,10574],{"id":10573},"第二步二分查找","第二步：二分查找",[15,10576,10577,10578,10581,10582,10585],{},"在按起始 IP 排序的段列表里做",[25,10579,10580],{},"二分查找","，找到满足 ",[36,10583,10584],{},"段的起始IP ≤ 目标整数 ≤ 段的结束IP"," 的那一段。",[15,10587,10588],{},"二分查找的复杂度是 O(log N)，N 是几十万条，所以很快——这就是「不靠网络也能毫秒返回」的核心。",[162,10590,10592],{"id":10591},"第三步xdb-文件的两级结构","第三步：.xdb 文件的两级结构",[15,10594,10595,10596,10598],{},"ip2region 的 ",[36,10597,10537],{}," 不是简单顺序文件，内部分两个区，进一步加速：",[167,10600,10603],{"className":10601,"code":10602,"language":2692,"meta":172},[2690],"┌─────────────────────────────┐\n│  索引区 (Index)             │  \u002F\u002F 每段一条定长记录：startIP \u002F endIP \u002F 数据区指针 + 数据长度\n│  …… 二分快速定位段 ……       │\n├─────────────────────────────┤\n│  数据区 (Data)              │  \u002F\u002F 真正存放各段的地点字符串\n│  ……                         │\n└─────────────────────────────┘\n",[36,10604,10602],{"__ignoreMap":172},[15,10606,10607],{},"先二分索引区定位到「是哪一段」，再按指针去数据区读出地点字符串，一次磁盘读就能拿到结果。",[162,10609,10611],{"id":10610},"为什么离线数据不会很快过期","为什么「离线数据」不会很快过期",[15,10613,10614,10615,10618],{},"IP 归属取决于",[25,10616,10617],{},"注册机构和运营商的分配","，一个网段通常长期稳定属于某地区、某运营商。所以：",[859,10620,10621,10624,10634],{},[22,10622,10623],{},"下载一次数据文件，可以用很久；",[22,10625,10626,10627,10633],{},"归属偶尔变化时，",[25,10628,10629,10630,10632],{},"每月更新一次 ",[36,10631,10537],{}," 文件","即可（替换文件、不用重启服务，见下文实战）；",[22,10635,10636],{},"数据源来自 IANA\u002FAPNIC 等机构的分配表与公开归属信息，不是实时探测。",[11,10638,10639],{"id":10639},"主流查询方案对比",[15,10641,10642],{},"实际开发中，查询 IP 属地主要有三条路线。",[162,10644,10646],{"id":10645},"方案一ip2region纯本地离线推荐","方案一：ip2region（纯本地离线，推荐）",[167,10648,10650],{"className":4773,"code":10649,"language":1522,"meta":172,"style":172},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\nregion, err := searcher.SearchByStr(ip)\n\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[36,10651,10652,10657,10673],{"__ignoreMap":172},[176,10653,10654],{"class":178,"line":179},[176,10655,10656],{"class":247},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\n",[176,10658,10659,10662,10664,10667,10670],{"class":178,"line":244},[176,10660,10661],{"class":228},"region, err ",[176,10663,4799],{"class":224},[176,10665,10666],{"class":228}," searcher.",[176,10668,10669],{"class":182},"SearchByStr",[176,10671,10672],{"class":228},"(ip)\n",[176,10674,10675],{"class":178,"line":251},[176,10676,10677],{"class":247},"\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[48,10679,10680,10688],{},[51,10681,10682],{},[54,10683,10684,10686],{},[57,10685,4033],{},[57,10687,4036],{},[70,10689,10690,10698,10706],{},[54,10691,10692,10695],{},[75,10693,10694],{},"免费、无网络依赖、无 QPS 限制",[75,10696,10697],{},"段级粒度，个别段可能显示到相邻地区",[54,10699,10700,10703],{},[75,10701,10702],{},"毫秒级查询，永不因外部服务挂掉",[75,10704,10705],{},"数据按月更新，最新分配的段可能短暂缺失",[54,10707,10708,10711],{},[75,10709,10710],{},"数据文件可随版本更新，不重启",[75,10712,10713,10714],{},"只返回省市区+运营商，无经纬度；",[25,10715,10716],{},"仅支持 IPv4",[15,10718,10719,10722],{},[25,10720,10721],{},"适用","：博客\u002F后台的「登录记录显示归属地」这类展示用途。",[162,10724,10726],{"id":10725},"方案二在线-apiip-api-高德-腾讯位置服务","方案二：在线 API（ip-api \u002F 高德 \u002F 腾讯位置服务）",[167,10728,10730],{"className":4773,"code":10729,"language":1522,"meta":172,"style":172},"\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",[36,10731,10732,10737,10765,10775,10806,10822,10835,10845,10849,10861],{"__ignoreMap":172},[176,10733,10734],{"class":178,"line":179},[176,10735,10736],{"class":247},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\n",[176,10738,10739,10742,10744,10746,10749,10752,10755,10758,10760,10762],{"class":178,"line":244},[176,10740,10741],{"class":228},"ctx, cancel ",[176,10743,4799],{"class":224},[176,10745,6265],{"class":228},[176,10747,10748],{"class":182},"WithTimeout",[176,10750,10751],{"class":228},"(context.",[176,10753,10754],{"class":182},"Background",[176,10756,10757],{"class":228},"(), ",[176,10759,3318],{"class":294},[176,10761,5004],{"class":224},[176,10763,10764],{"class":228},"time.Second)\n",[176,10766,10767,10770,10773],{"class":178,"line":251},[176,10768,10769],{"class":224},"defer",[176,10771,10772],{"class":182}," cancel",[176,10774,3555],{"class":228},[176,10776,10777,10780,10782,10785,10788,10791,10794,10796,10799,10801,10804],{"class":178,"line":259},[176,10778,10779],{"class":228},"req, _ ",[176,10781,4799],{"class":224},[176,10783,10784],{"class":228}," http.",[176,10786,10787],{"class":182},"NewRequestWithContext",[176,10789,10790],{"class":228},"(ctx, ",[176,10792,10793],{"class":186},"\"GET\"",[176,10795,612],{"class":228},[176,10797,10798],{"class":186},"\"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\"",[176,10800,612],{"class":228},[176,10802,10803],{"class":294},"nil",[176,10805,1243],{"class":228},[176,10807,10808,10811,10813,10816,10819],{"class":178,"line":266},[176,10809,10810],{"class":228},"resp, err ",[176,10812,4799],{"class":224},[176,10814,10815],{"class":228}," http.DefaultClient.",[176,10817,10818],{"class":182},"Do",[176,10820,10821],{"class":228},"(req)\n",[176,10823,10824,10827,10829,10831,10833],{"class":178,"line":284},[176,10825,10826],{"class":224},"if",[176,10828,5212],{"class":228},[176,10830,5215],{"class":224},[176,10832,5218],{"class":294},[176,10834,281],{"class":228},[176,10836,10837,10839,10842],{"class":178,"line":298},[176,10838,5029],{"class":224},[176,10840,10841],{"class":186}," \"未知\"",[176,10843,10844],{"class":247}," \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n",[176,10846,10847],{"class":178,"line":304},[176,10848,301],{"class":228},[176,10850,10851,10853,10856,10859],{"class":178,"line":309},[176,10852,10769],{"class":224},[176,10854,10855],{"class":228}," resp.Body.",[176,10857,10858],{"class":182},"Close",[176,10860,3555],{"class":228},[176,10862,10863],{"class":178,"line":350},[176,10864,10865],{"class":247},"\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[48,10867,10868,10876],{},[51,10869,10870],{},[54,10871,10872,10874],{},[57,10873,4033],{},[57,10875,4036],{},[70,10877,10878,10886,10894],{},[54,10879,10880,10883],{},[75,10881,10882],{},"数据最新、可能更精确",[75,10884,10885],{},"有频率限制（免费版限 QPS）",[54,10887,10888,10891],{},[75,10889,10890],{},"部分服务还返回经纬度\u002F时区",[75,10892,10893],{},"依赖第三方，断网\u002F被墙\u002F限流时拿不到",[54,10895,10896,10899],{},[75,10897,10898],{},"无需维护数据文件",[75,10900,10901],{},"每次查询都要发一次 HTTP 请求",[15,10903,10904,10906,10907,10910],{},[25,10905,10721],{},"：对精度要求高、且能接受外部依赖与限流的场景。",[25,10908,10909],{},"务必加超时和降级","，否则第三方一抖，你自己的页面就卡住。",[162,10912,10914],{"id":10913},"方案三maxmind-geolite2离线-mmdb","方案三：MaxMind GeoLite2（离线 .mmdb）",[48,10916,10917,10925],{},[51,10918,10919],{},[54,10920,10921,10923],{},[57,10922,4033],{},[57,10924,4036],{},[70,10926,10927,10935,10947],{},[54,10928,10929,10932],{},[75,10930,10931],{},"全球数据，覆盖广、精度较好",[75,10933,10934],{},"免费但需注册账号并遵守 EULA；商用需 GeoIP2 商业授权",[54,10936,10937,10944],{},[75,10938,10939,10940,10943],{},"标准 ",[36,10941,10942],{},".mmdb"," 格式，各语言都有库，支持 IPv6",[75,10945,10946],{},"国内 IP 的省市粒度有时不如 ip2region 细",[54,10948,10949,10952],{},[75,10950,10951],{},"离线查询",[75,10953,10954],{},"数据同样要定期更新",[15,10956,10957,10959],{},[25,10958,10721],{},"：面向全球用户的场景。",[11,10961,10963],{"id":10962},"ipv6本地离线方案的一个盲点","IPv6：本地离线方案的一个盲点",[15,10965,10966,10967,10970,10971,10974],{},"前面聊的都是 IPv4（32 位）。但家宽和移动网络越来越多地",[25,10968,10969],{},"只下发 IPv6","（128 位）地址，而 ",[25,10972,10973],{},"ip2region 目前只支持 IPv4","——拿到一个纯 IPv6 地址是查不出归属地的，本地库会返回空结果。",[15,10976,10977],{},"实际影响与常见应对：",[19,10979,10980,10990,10996,11006],{},[22,10981,10982,10985,10986,10989],{},[25,10983,10984],{},"判断版本","：Go 里 ",[36,10987,10988],{},"net.ParseIP(ip).To4() != nil"," 就是 IPv4，先判断再查询；",[22,10991,10992,10995],{},[25,10993,10994],{},"降级展示","：IPv6 直接显示「IPv6 地址」，不显示属地，诚实第一；",[22,10997,10998,11001,11002,11005],{},[25,10999,11000],{},"优先取 IPv4","：有反向代理时，从 ",[36,11003,11004],{},"X-Forwarded-For"," 里挑一个 IPv4 来查；",[22,11007,11008,11011],{},[25,11009,11010],{},"需要 v6 归属","：换支持 IPv6 的数据源（如 MaxMind GeoLite2）。",[15,11013,11014],{},"这一点在接入时很容易漏掉，上线前记得拿一个纯 IPv6 地址回归一遍。",[11,11016,11018],{"id":11017},"实战给登录记录加-ip-属地","实战：给登录记录加 IP 属地",[15,11020,11021],{},"subLog 博客的登录记录就用了 ip2region。完整接入一共四步。",[162,11023,11025],{"id":11024},"_1-进程启动时加载数据文件","1. 进程启动时加载数据文件",[167,11027,11029],{"className":4773,"code":11028,"language":1522,"meta":172,"style":172},"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",[36,11030,11031,11037,11046,11055,11059,11063,11068,11095,11099,11112,11134,11146,11153,11157,11173,11185,11191,11195,11206,11213],{"__ignoreMap":172},[176,11032,11033,11035],{"class":178,"line":179},[176,11034,225],{"class":224},[176,11036,676],{"class":228},[176,11038,11039,11041,11044],{"class":178,"line":244},[176,11040,7589],{"class":186},[176,11042,11043],{"class":182},"sync\u002Fatomic",[176,11045,1619],{"class":186},[176,11047,11048,11050,11053],{"class":178,"line":251},[176,11049,7589],{"class":186},[176,11051,11052],{"class":182},"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb",[176,11054,1619],{"class":186},[176,11056,11057],{"class":178,"line":259},[176,11058,1243],{"class":228},[176,11060,11061],{"class":178,"line":266},[176,11062,263],{"emptyLinePlaceholder":262},[176,11064,11065],{"class":178,"line":284},[176,11066,11067],{"class":247},"\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\n",[176,11069,11070,11072,11075,11078,11080,11083,11085,11088,11090,11093],{"class":178,"line":298},[176,11071,7670],{"class":224},[176,11073,11074],{"class":228}," searcher ",[176,11076,11077],{"class":182},"atomic",[176,11079,408],{"class":228},[176,11081,11082],{"class":182},"Pointer",[176,11084,4161],{"class":228},[176,11086,11087],{"class":182},"xdb",[176,11089,408],{"class":228},[176,11091,11092],{"class":182},"Searcher",[176,11094,1835],{"class":228},[176,11096,11097],{"class":178,"line":304},[176,11098,263],{"emptyLinePlaceholder":262},[176,11100,11101,11103,11106,11108,11110],{"class":178,"line":309},[176,11102,4973],{"class":224},[176,11104,11105],{"class":182}," initSearcher",[176,11107,6214],{"class":228},[176,11109,5017],{"class":224},[176,11111,281],{"class":228},[176,11113,11114,11117,11119,11122,11124,11126,11129,11131],{"class":178,"line":350},[176,11115,11116],{"class":228},"    content, err ",[176,11118,4799],{"class":224},[176,11120,11121],{"class":228}," os.",[176,11123,7809],{"class":182},[176,11125,1237],{"class":228},[176,11127,11128],{"class":186},"\".\u002Fdata\u002Fip2region.xdb\"",[176,11130,341],{"class":228},[176,11132,11133],{"class":247},"\u002F\u002F 约 11MB，一次性读入内存\n",[176,11135,11136,11138,11140,11142,11144],{"class":178,"line":362},[176,11137,3441],{"class":224},[176,11139,5212],{"class":228},[176,11141,5215],{"class":224},[176,11143,5218],{"class":294},[176,11145,281],{"class":228},[176,11147,11148,11150],{"class":178,"line":383},[176,11149,5225],{"class":224},[176,11151,11152],{"class":228}," err\n",[176,11154,11155],{"class":178,"line":389},[176,11156,3329],{"class":228},[176,11158,11159,11162,11164,11167,11170],{"class":178,"line":395},[176,11160,11161],{"class":228},"    s, err ",[176,11163,4799],{"class":224},[176,11165,11166],{"class":228}," xdb.",[176,11168,11169],{"class":182},"NewWithBuffer",[176,11171,11172],{"class":228},"(content)\n",[176,11174,11175,11177,11179,11181,11183],{"class":178,"line":420},[176,11176,3441],{"class":224},[176,11178,5212],{"class":228},[176,11180,5215],{"class":224},[176,11182,5218],{"class":294},[176,11184,281],{"class":228},[176,11186,11187,11189],{"class":178,"line":426},[176,11188,5225],{"class":224},[176,11190,11152],{"class":228},[176,11192,11193],{"class":178,"line":750},[176,11194,3329],{"class":228},[176,11196,11197,11200,11203],{"class":178,"line":761},[176,11198,11199],{"class":228},"    searcher.",[176,11201,11202],{"class":182},"Store",[176,11204,11205],{"class":228},"(s)\n",[176,11207,11208,11210],{"class":178,"line":772},[176,11209,5029],{"class":224},[176,11211,11212],{"class":294}," nil\n",[176,11214,11215],{"class":178,"line":778},[176,11216,301],{"class":228},[162,11218,11220],{"id":11219},"_2-查询一次","2. 查询一次",[167,11222,11224],{"className":4773,"code":11223,"language":1522,"meta":172,"style":172},"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",[36,11225,11226,11248,11267,11279,11286,11290,11300],{"__ignoreMap":172},[176,11227,11228,11230,11233,11235,11238,11241,11243,11246],{"class":178,"line":179},[176,11229,4973],{"class":224},[176,11231,11232],{"class":182}," ipRegion",[176,11234,1237],{"class":228},[176,11236,11237],{"class":287},"ip",[176,11239,11240],{"class":224}," string",[176,11242,341],{"class":228},[176,11244,11245],{"class":224},"string",[176,11247,281],{"class":228},[176,11249,11250,11253,11255,11257,11260,11263,11265],{"class":178,"line":244},[176,11251,11252],{"class":228},"    region, err ",[176,11254,4799],{"class":224},[176,11256,10666],{"class":228},[176,11258,11259],{"class":182},"Load",[176,11261,11262],{"class":228},"().",[176,11264,10669],{"class":182},[176,11266,10672],{"class":228},[176,11268,11269,11271,11273,11275,11277],{"class":178,"line":251},[176,11270,3441],{"class":224},[176,11272,5212],{"class":228},[176,11274,5215],{"class":224},[176,11276,5218],{"class":294},[176,11278,281],{"class":228},[176,11280,11281,11283],{"class":178,"line":259},[176,11282,5225],{"class":224},[176,11284,11285],{"class":186}," \"未知\"\n",[176,11287,11288],{"class":178,"line":266},[176,11289,3329],{"class":228},[176,11291,11292,11294,11297],{"class":178,"line":284},[176,11293,5029],{"class":224},[176,11295,11296],{"class":228}," region ",[176,11298,11299],{"class":247},"\u002F\u002F \"中国|福建省|福州市|移动\"\n",[176,11301,11302],{"class":178,"line":298},[176,11303,301],{"class":228},[162,11305,11307],{"id":11306},"_3-加一层内存缓存","3. 加一层内存缓存",[15,11309,11310,11311,11314],{},"登录记录里同一个 IP 可能反复出现，用 ",[36,11312,11313],{},"sync.Map"," 按 IP 缓存结果，一次查询终身复用：",[167,11316,11318],{"className":4773,"code":11317,"language":1522,"meta":172,"style":172},"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",[36,11319,11320,11335,11339,11358,11375,11386,11390,11401,11411,11418],{"__ignoreMap":172},[176,11321,11322,11324,11327,11330,11332],{"class":178,"line":179},[176,11323,7670],{"class":224},[176,11325,11326],{"class":228}," regionCache ",[176,11328,11329],{"class":182},"sync",[176,11331,408],{"class":228},[176,11333,11334],{"class":182},"Map\n",[176,11336,11337],{"class":178,"line":244},[176,11338,263],{"emptyLinePlaceholder":262},[176,11340,11341,11343,11346,11348,11350,11352,11354,11356],{"class":178,"line":251},[176,11342,4973],{"class":224},[176,11344,11345],{"class":182}," ipRegionCached",[176,11347,1237],{"class":228},[176,11349,11237],{"class":287},[176,11351,11240],{"class":224},[176,11353,341],{"class":228},[176,11355,11245],{"class":224},[176,11357,281],{"class":228},[176,11359,11360,11362,11365,11367,11370,11372],{"class":178,"line":259},[176,11361,3441],{"class":224},[176,11363,11364],{"class":228}," v, ok ",[176,11366,4799],{"class":224},[176,11368,11369],{"class":228}," regionCache.",[176,11371,11259],{"class":182},[176,11373,11374],{"class":228},"(ip); ok {\n",[176,11376,11377,11379,11382,11384],{"class":178,"line":266},[176,11378,5225],{"class":224},[176,11380,11381],{"class":228}," v.(",[176,11383,11245],{"class":224},[176,11385,1243],{"class":228},[176,11387,11388],{"class":178,"line":284},[176,11389,3329],{"class":228},[176,11391,11392,11395,11397,11399],{"class":178,"line":298},[176,11393,11394],{"class":228},"    region ",[176,11396,4799],{"class":224},[176,11398,11232],{"class":182},[176,11400,10672],{"class":228},[176,11402,11403,11406,11408],{"class":178,"line":304},[176,11404,11405],{"class":228},"    regionCache.",[176,11407,11202],{"class":182},[176,11409,11410],{"class":228},"(ip, region)\n",[176,11412,11413,11415],{"class":178,"line":309},[176,11414,5029],{"class":224},[176,11416,11417],{"class":228}," region\n",[176,11419,11420],{"class":178,"line":350},[176,11421,301],{"class":228},[162,11423,11425],{"id":11424},"_4-数据热更新不用重启","4. 数据热更新（不用重启）",[15,11427,11428,11429,11431,11432,11434],{},"ip2region 每月发布新数据。下载新文件后重新 ",[36,11430,11169],{}," 并 ",[36,11433,11202],{}," 覆盖即可，进程完全不用重启：",[167,11436,11438],{"className":4773,"code":11437,"language":1522,"meta":172,"style":172},"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",[36,11439,11440,11464,11477,11489,11495,11499,11511,11517],{"__ignoreMap":172},[176,11441,11442,11444,11447,11449,11452,11455,11458,11460,11462],{"class":178,"line":179},[176,11443,4973],{"class":224},[176,11445,11446],{"class":182}," reloadSearcher",[176,11448,1237],{"class":228},[176,11450,11451],{"class":287},"newContent",[176,11453,11454],{"class":228}," []",[176,11456,11457],{"class":224},"byte",[176,11459,341],{"class":228},[176,11461,5017],{"class":224},[176,11463,281],{"class":228},[176,11465,11466,11468,11470,11472,11474],{"class":178,"line":244},[176,11467,11161],{"class":228},[176,11469,4799],{"class":224},[176,11471,11166],{"class":228},[176,11473,11169],{"class":182},[176,11475,11476],{"class":228},"(newContent)\n",[176,11478,11479,11481,11483,11485,11487],{"class":178,"line":251},[176,11480,3441],{"class":224},[176,11482,5212],{"class":228},[176,11484,5215],{"class":224},[176,11486,5218],{"class":294},[176,11488,281],{"class":228},[176,11490,11491,11493],{"class":178,"line":259},[176,11492,5225],{"class":224},[176,11494,11152],{"class":228},[176,11496,11497],{"class":178,"line":266},[176,11498,3329],{"class":228},[176,11500,11501,11503,11505,11508],{"class":178,"line":284},[176,11502,11199],{"class":228},[176,11504,11202],{"class":182},[176,11506,11507],{"class":228},"(s) ",[176,11509,11510],{"class":247},"\u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n",[176,11512,11513,11515],{"class":178,"line":298},[176,11514,5029],{"class":224},[176,11516,11212],{"class":294},[176,11518,11519],{"class":178,"line":304},[176,11520,301],{"class":228},[11,11522,11523],{"id":11523},"局限与诚实提醒",[15,11525,11526],{},"无论用哪种方案，都要清楚 IP 属地的边界：",[859,11528,11529,11535,11541,11547,11553],{},[22,11530,11531,11534],{},[25,11532,11533],{},"段级精度","：网段归属是「绝大多数」准确，个别 IP（大型企业自建段、跨省调度段）可能显示到相邻地区或只到省。",[22,11536,11537,11540],{},[25,11538,11539],{},"运营商级，不是定位","：拿到的是「省｜市｜运营商」，不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位，纯 IP 归属做不到。",[22,11542,11543,11546],{},[25,11544,11545],{},"动态 IP","：家宽通常是动态 IP，重拨会变，所以「上次登录在福州，这次在广州」不一定是异常，可能是 IP 变了。",[22,11548,11549,11552],{},[25,11550,11551],{},"别用于安全判定","：用 IP 段做封禁或风控判定风险自负，段级数据不适合当精确依据。",[22,11554,11555,11558,11559,11562],{},[25,11556,11557],{},"隐私与合规","：IP 地址属于个人信息（《个人信息保护法》）。归属地展示建议只出现在",[25,11560,11561],{},"用户本人可见","的页面（登录记录、个人中心）；若在评论区等公开场合展示，建议只显示到省市并明确告知用户。",[11,11564,11565],{"id":11565},"小结",[15,11567,1341],{},[10490,11569,11570],{},[15,11571,11572,11575],{},[25,11573,11574],{},"IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。"," 它查的是网段归属，不是精确位置。",[859,11577,11578,11584,11591],{},[22,11579,11580,11581,11583],{},"想省心、免费、稳定，用 ",[25,11582,10554],{}," 本地离线查询（注意 IPv6 盲点）；",[22,11585,11586,11587,11590],{},"想要最新最细、能接受外部依赖，用",[25,11588,11589],{},"在线 API","（记得超时降级）；",[22,11592,11593,11594,910],{},"面向全球用户、要 IPv6 覆盖，用 ",[25,11595,11596],{},"MaxMind GeoLite2",[15,11598,11599],{},"对个人博客「登录记录显示归属地」这种场景，本地离线方案通常是性价比最高的选择。",[1346,11601,11602],{},"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":172,"searchDepth":244,"depth":251,"links":11604},[11605,11606,11607,11613,11618,11619,11625,11626],{"id":9910,"depth":244,"text":9910},{"id":10501,"depth":244,"text":10502},{"id":10548,"depth":244,"text":10548,"children":11608},[11609,11610,11611,11612],{"id":10558,"depth":251,"text":10559},{"id":10573,"depth":251,"text":10574},{"id":10591,"depth":251,"text":10592},{"id":10610,"depth":251,"text":10611},{"id":10639,"depth":244,"text":10639,"children":11614},[11615,11616,11617],{"id":10645,"depth":251,"text":10646},{"id":10725,"depth":251,"text":10726},{"id":10913,"depth":251,"text":10914},{"id":10962,"depth":244,"text":10963},{"id":11017,"depth":244,"text":11018,"children":11620},[11621,11622,11623,11624],{"id":11024,"depth":251,"text":11025},{"id":11219,"depth":251,"text":11220},{"id":11306,"depth":251,"text":11307},{"id":11424,"depth":251,"text":11425},{"id":11523,"depth":244,"text":11523},{"id":11565,"depth":244,"text":11565},"科普 IP 属地查询的原理：IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级，对比 ip2region、在线 API 与 MaxMind 三种方案的优劣，并附登录记录场景的 Go 实战与 IPv6 盲点提醒。","\u002Fposts\u002Fip-geolocation-guide\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fip-geolocation-guide","2026-08-07T12:00:00",{"title":10464,"description":11627},"posts\u002Fip-geolocation-guide\u002Findex",[11635,11636,11637],"IP","网络","科普","RY1UPgJaiKsjpC51uqMyG7w_GpCRS13_CX5j1hDD_dM",{"id":11640,"title":11641,"author":6,"body":11642,"description":13159,"difficulty":2169,"draft":1368,"extension":1369,"image":13160,"meta":13161,"navigation":262,"path":13162,"pinned":1368,"published":1373,"seo":13163,"series":6729,"seriesOrder":251,"stem":13164,"tags":13165,"updated":1382,"__hash__":13168},"posts\u002Fposts\u002Fgo-embed-react-single-binary\u002Findex.md","Go 单文件打包进阶：React 前端 + 交叉编译，一次踩完所有坑",{"type":8,"value":11643,"toc":13138},[11644,11646,11657,11668,11675,11678,11714,11716,11725,11727,11731,11741,11784,11824,11842,11846,11867,11898,11902,12194,12197,12235,12239,12582,12585,12589,12786,12796,12798,12809,12823,12836,12847,12851,12858,12874,12886,12891,12895,12911,12916,12961,12975,12985,12989,13004,13013,13035,13044,13048,13058,13060,13087,13096,13098,13101,13128,13135],[11,11645,3237],{"id":3237},[15,11647,11648,11649,11652,11653,11656],{},"这是「Go 单文件打包」系列第三篇。上一篇（",[3190,11650,11651],{"href":8484},"把前端塞进二进制","）记录了一个 Gin + Vue 3 项目的 embed 方案。这次换了个项目继续踩坑：",[25,11654,11655],{},"Magic Img","——一个自托管、本地优先的 AI 生图提示词管理工具，技术栈是：",[859,11658,11659,11662],{},[22,11660,11661],{},"前端：React 19 + TypeScript + Vite + Tailwind v4（UI 库 Magic UI + shadcn\u002Fui）",[22,11663,11664,11665,7272],{},"后端：Go + Gin + GORM + SQLite（",[25,11666,11667],{},"modernc 纯 Go 驱动，无 cgo",[15,11669,11670,11671,11674],{},"目标一样：",[25,11672,11673],{},"一个二进制拖到任何机器上就能跑","——前端、API、数据库全部内嵌。",[15,11676,11677],{},"和上篇不同的地方有三个，正好是这篇要讲的重点：",[19,11679,11680,11694,11707],{},[22,11681,11682,11683,11689,11690,11693],{},"前端用了 ",[25,11684,11685,11686],{},"HashRouter + Vite base ",[36,11687,11688],{},"\u002Fdist","，服务端",[25,11691,11692],{},"不再需要 SPA fallback","（上篇坑一的新解法）；",[22,11695,11696,11697,11703,11704,11706],{},"embed 用 ",[25,11698,11699,11702],{},[36,11700,11701],{},"all:"," 前缀","一劳永逸解决 ",[36,11705,8180],{}," 开头文件被排除的问题（上篇坑二的升级解法）；",[22,11708,11709,11710,11713],{},"需要",[25,11711,11712],{},"在 Windows 上交叉编译 Linux 产物","，踩了「生成的文件无法在 Linux 执行」的坑。",[11,11715,7538],{"id":7538},[15,11717,11718,11719,11721,11722,11724],{},"和上篇一致：",[36,11720,7544],{}," 在编译期把前端 ",[36,11723,7548],{}," 塞进二进制，运行时由 Go 直接提供静态服务。差异在于路由策略和缓存策略，后面细说。",[11,11726,7552],{"id":7552},[162,11728,11730],{"id":11729},"_1-前端hash-路由-base-dist","1. 前端：hash 路由 + base \u002Fdist",[15,11732,11733,11734,11737,11738,1032],{},"vite.config.ts 里固定 ",[36,11735,11736],{},"base: \"\u002Fdist\u002F\"","，路由用 ",[36,11739,11740],{},"HashRouter",[167,11742,11744],{"className":8230,"code":11743,"language":8232,"meta":172,"style":172},"\u002F\u002F vite.config.ts\nexport default defineConfig({\n  base: \"\u002Fdist\u002F\",   \u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n  ...\n})\n",[36,11745,11746,11750,11762,11776,11780],{"__ignoreMap":172},[176,11747,11748],{"class":178,"line":179},[176,11749,8239],{"class":247},[176,11751,11752,11754,11756,11759],{"class":178,"line":244},[176,11753,1487],{"class":224},[176,11755,4094],{"class":224},[176,11757,11758],{"class":182}," defineConfig",[176,11760,11761],{"class":228},"({\n",[176,11763,11764,11767,11770,11773],{"class":178,"line":251},[176,11765,11766],{"class":228},"  base: ",[176,11768,11769],{"class":186},"\"\u002Fdist\u002F\"",[176,11771,11772],{"class":228},",   ",[176,11774,11775],{"class":247},"\u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n",[176,11777,11778],{"class":178,"line":259},[176,11779,1155],{"class":224},[176,11781,11782],{"class":178,"line":266},[176,11783,3900],{"class":228},[167,11785,11787],{"className":215,"code":11786,"language":217,"meta":172,"style":172},"\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n\u003CHashRouter>\n  \u003CRoutes>...\u003C\u002FRoutes>\n\u003C\u002FHashRouter>\n",[36,11788,11789,11794,11802,11816],{"__ignoreMap":172},[176,11790,11791],{"class":178,"line":179},[176,11792,11793],{"class":247},"\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n",[176,11795,11796,11798,11800],{"class":178,"line":244},[176,11797,443],{"class":228},[176,11799,11740],{"class":294},[176,11801,712],{"class":228},[176,11803,11804,11806,11809,11812,11814],{"class":178,"line":251},[176,11805,3927],{"class":228},[176,11807,11808],{"class":294},"Routes",[176,11810,11811],{"class":228},">...\u003C\u002F",[176,11813,11808],{"class":294},[176,11815,712],{"class":228},[176,11817,11818,11820,11822],{"class":178,"line":259},[176,11819,3905],{"class":228},[176,11821,11740],{"class":294},[176,11823,712],{"class":228},[15,11825,11826,11829,11830,11833,11834,11837,11838,11841],{},[25,11827,11828],{},"为什么这样选","：history 路由需要服务端把未知路径全部 fallback 回 ",[36,11831,11832],{},"index.html","（上篇坑一）。hash 路由下浏览器请求的永远是 ",[36,11835,11836],{},"\u002Fdist\u002F"," 这一个入口，",[25,11839,11840],{},"静态服务直接返回 index.html 就完事，一个 fallback 都不用写","。内嵌场景里这是最省心的组合。",[162,11843,11845],{"id":11844},"_2-构建产物复制进后端目录","2. 构建产物复制进后端目录",[15,11847,11848,11851,11852,11854,11855,11858,11859,11862,11863,11866],{},[36,11849,11850],{},"npm run build"," 除了编译，还会把 ",[36,11853,7548],{}," 复制到 ",[36,11856,11857],{},"backend\u002Fweb\u002Fdist","（一个小脚本 ",[36,11860,11861],{},"scripts\u002Fcopy-dist.mjs"," 干这个活），供 ",[36,11864,11865],{},"go:embed"," 使用：",[167,11868,11870],{"className":6557,"code":11869,"language":6559,"meta":172,"style":172},"\u002F\u002F frontend\u002Fpackage.json\n\"scripts\": {\n  \"build\": \"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n}\n",[36,11871,11872,11877,11884,11894],{"__ignoreMap":172},[176,11873,11874],{"class":178,"line":179},[176,11875,11876],{"class":247},"\u002F\u002F frontend\u002Fpackage.json\n",[176,11878,11879,11882],{"class":178,"line":244},[176,11880,11881],{"class":186},"\"scripts\"",[176,11883,3288],{"class":228},[176,11885,11886,11889,11891],{"class":178,"line":251},[176,11887,11888],{"class":294},"  \"build\"",[176,11890,3074],{"class":228},[176,11892,11893],{"class":186},"\"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n",[176,11895,11896],{"class":178,"line":259},[176,11897,301],{"class":228},[162,11899,11901],{"id":11900},"_3-后端embed-httpfilesystem-包装","3. 后端：embed + http.FileSystem 包装",[167,11903,11905],{"className":4773,"code":11904,"language":1522,"meta":172,"style":172},"\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",[36,11906,11907,11912,11918,11922,11928,11936,11944,11952,11956,11960,11965,11978,11982,11987,12024,12028,12040,12048,12061,12065,12069,12108,12122,12132,12142,12169,12173,12190],{"__ignoreMap":172},[176,11908,11909],{"class":178,"line":179},[176,11910,11911],{"class":247},"\u002F\u002F backend\u002Fweb\u002Fembed.go\n",[176,11913,11914,11916],{"class":178,"line":244},[176,11915,7571],{"class":224},[176,11917,7574],{"class":182},[176,11919,11920],{"class":178,"line":251},[176,11921,263],{"emptyLinePlaceholder":262},[176,11923,11924,11926],{"class":178,"line":259},[176,11925,225],{"class":224},[176,11927,676],{"class":228},[176,11929,11930,11932,11934],{"class":178,"line":266},[176,11931,7589],{"class":186},[176,11933,7592],{"class":182},[176,11935,1619],{"class":186},[176,11937,11938,11940,11942],{"class":178,"line":284},[176,11939,7589],{"class":186},[176,11941,7619],{"class":182},[176,11943,1619],{"class":186},[176,11945,11946,11948,11950],{"class":178,"line":298},[176,11947,7589],{"class":186},[176,11949,7637],{"class":182},[176,11951,1619],{"class":186},[176,11953,11954],{"class":178,"line":304},[176,11955,1243],{"class":228},[176,11957,11958],{"class":178,"line":309},[176,11959,263],{"emptyLinePlaceholder":262},[176,11961,11962],{"class":178,"line":350},[176,11963,11964],{"class":247},"\u002F\u002Fgo:embed all:dist\n",[176,11966,11967,11969,11972,11974,11976],{"class":178,"line":362},[176,11968,7670],{"class":224},[176,11970,11971],{"class":228}," distFS ",[176,11973,7592],{"class":182},[176,11975,408],{"class":228},[176,11977,7680],{"class":182},[176,11979,11980],{"class":178,"line":383},[176,11981,263],{"emptyLinePlaceholder":262},[176,11983,11984],{"class":178,"line":389},[176,11985,11986],{"class":247},"\u002F\u002F Dist 内嵌前端静态文件系统\n",[176,11988,11989,11991,11994,11997,11999,12002,12004,12007,12010,12013,12015,12018,12021],{"class":178,"line":395},[176,11990,7670],{"class":224},[176,11992,11993],{"class":228}," Dist ",[176,11995,11996],{"class":182},"http",[176,11998,408],{"class":228},[176,12000,12001],{"class":182},"FileSystem",[176,12003,1223],{"class":224},[176,12005,12006],{"class":224}," &",[176,12008,12009],{"class":182},"fileSystem",[176,12011,12012],{"class":228},"{prefix: ",[176,12014,7720],{"class":186},[176,12016,12017],{"class":228},", fs: ",[176,12019,12020],{"class":224},"&",[176,12022,12023],{"class":228},"distFS}\n",[176,12025,12026],{"class":178,"line":420},[176,12027,263],{"emptyLinePlaceholder":262},[176,12029,12030,12032,12035,12038],{"class":178,"line":426},[176,12031,232],{"class":224},[176,12033,12034],{"class":182}," fileSystem",[176,12036,12037],{"class":224}," struct",[176,12039,281],{"class":228},[176,12041,12042,12045],{"class":178,"line":750},[176,12043,12044],{"class":228},"    prefix ",[176,12046,12047],{"class":224},"string\n",[176,12049,12050,12053,12055,12057,12059],{"class":178,"line":761},[176,12051,12052],{"class":228},"    fs     ",[176,12054,5004],{"class":224},[176,12056,7592],{"class":182},[176,12058,408],{"class":228},[176,12060,7680],{"class":182},[176,12062,12063],{"class":178,"line":772},[176,12064,301],{"class":228},[176,12066,12067],{"class":178,"line":778},[176,12068,263],{"emptyLinePlaceholder":262},[176,12070,12071,12073,12075,12078,12080,12082,12084,12087,12089,12091,12093,12095,12097,12099,12102,12104,12106],{"class":178,"line":789},[176,12072,4973],{"class":224},[176,12074,1062],{"class":228},[176,12076,12077],{"class":287},"f ",[176,12079,5004],{"class":224},[176,12081,12009],{"class":182},[176,12083,341],{"class":228},[176,12085,12086],{"class":182},"Open",[176,12088,1237],{"class":228},[176,12090,8283],{"class":287},[176,12092,11240],{"class":224},[176,12094,5001],{"class":228},[176,12096,11996],{"class":182},[176,12098,408],{"class":228},[176,12100,12101],{"class":182},"File",[176,12103,612],{"class":228},[176,12105,5017],{"class":224},[176,12107,4601],{"class":228},[176,12109,12110,12112,12115,12117,12120],{"class":178,"line":807},[176,12111,3441],{"class":224},[176,12113,12114],{"class":228}," name ",[176,12116,5871],{"class":224},[176,12118,12119],{"class":186}," \"\u002F\"",[176,12121,281],{"class":228},[176,12123,12124,12127,12129],{"class":178,"line":813},[176,12125,12126],{"class":228},"        name ",[176,12128,326],{"class":224},[176,12130,12131],{"class":228}," f.prefix\n",[176,12133,12134,12137,12140],{"class":178,"line":823},[176,12135,12136],{"class":228},"    } ",[176,12138,12139],{"class":224},"else",[176,12141,281],{"class":228},[176,12143,12144,12146,12148,12151,12153,12155,12158,12160,12162,12165,12167],{"class":178,"line":833},[176,12145,12126],{"class":228},[176,12147,326],{"class":224},[176,12149,12150],{"class":228}," f.prefix ",[176,12152,4169],{"class":224},[176,12154,12119],{"class":186},[176,12156,12157],{"class":224}," +",[176,12159,5932],{"class":228},[176,12161,7758],{"class":182},[176,12163,12164],{"class":228},"(name, ",[176,12166,7764],{"class":186},[176,12168,1243],{"class":228},[176,12170,12171],{"class":178,"line":839},[176,12172,3329],{"class":228},[176,12174,12175,12177,12179,12182,12185,12187],{"class":178,"line":845},[176,12176,5029],{"class":224},[176,12178,10784],{"class":228},[176,12180,12181],{"class":182},"FS",[176,12183,12184],{"class":228},"(f.fs).",[176,12186,12086],{"class":182},[176,12188,12189],{"class":228},"(name)\n",[176,12191,12192],{"class":178,"line":851},[176,12193,301],{"class":228},[15,12195,12196],{},"两个细节：",[859,12198,12199,12222],{},[22,12200,12201,12209,12210,1193,12212,12214,12215,12217,12218,12221],{},[25,12202,12203,12206,12207,11702],{},[36,12204,12205],{},"all:dist"," 里的 ",[36,12208,11701],{},"：embed 目录时默认会排除 ",[36,12211,8180],{},[36,12213,408],{}," 开头的文件（上篇坑二就是被这个坑了，当时靠改 Vite 输出名绕开）。",[36,12216,11701],{}," 前缀表示「全部包含」，前端构建产物里那些 ",[36,12219,12220],{},"_xxx.js"," 助手文件直接安全落地，不用再改前端配置；",[22,12223,12224,12227,12228,12230,12231,12234],{},[36,12225,12226],{},"http.FileSystem"," 包装：加了一层 ",[36,12229,7548],{}," 前缀映射，",[36,12232,12233],{},"http.FileServer"," 和 Gin 都能直接用。",[162,12236,12238],{"id":12237},"_4-路由静态服务-缓存策略","4. 路由：静态服务 + 缓存策略",[167,12240,12242],{"className":4773,"code":12241,"language":1522,"meta":172,"style":172},"\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",[36,12243,12244,12249,12291,12303,12308,12323,12368,12397,12415,12436,12441,12460,12483,12488,12505,12509,12530,12534,12574,12578],{"__ignoreMap":172},[176,12245,12246],{"class":178,"line":179},[176,12247,12248],{"class":247},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\n",[176,12250,12251,12253,12256,12258,12261,12263,12266,12268,12271,12273,12276,12278,12280,12282,12284,12286,12289],{"class":178,"line":244},[176,12252,4973],{"class":224},[176,12254,12255],{"class":182}," NewRouter",[176,12257,1237],{"class":228},[176,12259,12260],{"class":287},"db",[176,12262,5597],{"class":224},[176,12264,12265],{"class":182},"gorm",[176,12267,408],{"class":228},[176,12269,12270],{"class":182},"DB",[176,12272,612],{"class":228},[176,12274,12275],{"class":287},"uploadDir",[176,12277,11240],{"class":224},[176,12279,341],{"class":228},[176,12281,5004],{"class":224},[176,12283,5600],{"class":182},[176,12285,408],{"class":228},[176,12287,12288],{"class":182},"Engine",[176,12290,281],{"class":228},[176,12292,12293,12295,12297,12299,12301],{"class":178,"line":251},[176,12294,5413],{"class":228},[176,12296,4799],{"class":224},[176,12298,5418],{"class":228},[176,12300,5421],{"class":182},[176,12302,3555],{"class":228},[176,12304,12305],{"class":178,"line":259},[176,12306,12307],{"class":224},"    ...\n",[176,12309,12310,12313,12315,12317,12320],{"class":178,"line":266},[176,12311,12312],{"class":228},"    dist ",[176,12314,4799],{"class":224},[176,12316,10784],{"class":228},[176,12318,12319],{"class":182},"FileServer",[176,12321,12322],{"class":228},"(web.Dist)\n",[176,12324,12325,12327,12329,12331,12334,12336,12338,12340,12342,12344,12346,12348,12350,12353,12356,12358,12361,12363,12365],{"class":178,"line":284},[176,12326,5551],{"class":228},[176,12328,5580],{"class":182},[176,12330,1237],{"class":228},[176,12332,12333],{"class":186},"\"\u002Fdist\"",[176,12335,612],{"class":228},[176,12337,4973],{"class":224},[176,12339,1237],{"class":228},[176,12341,5594],{"class":287},[176,12343,5597],{"class":224},[176,12345,5600],{"class":182},[176,12347,408],{"class":228},[176,12349,1054],{"class":182},[176,12351,12352],{"class":228},") { c.",[176,12354,12355],{"class":182},"Redirect",[176,12357,1237],{"class":228},[176,12359,12360],{"class":294},"302",[176,12362,612],{"class":228},[176,12364,11769],{"class":186},[176,12366,12367],{"class":228},") })\n",[176,12369,12370,12372,12374,12376,12379,12381,12383,12385,12387,12389,12391,12393,12395],{"class":178,"line":298},[176,12371,5551],{"class":228},[176,12373,5580],{"class":182},[176,12375,1237],{"class":228},[176,12377,12378],{"class":186},"\"\u002Fdist\u002F*filepath\"",[176,12380,612],{"class":228},[176,12382,4973],{"class":224},[176,12384,1237],{"class":228},[176,12386,5594],{"class":287},[176,12388,5597],{"class":224},[176,12390,5600],{"class":182},[176,12392,408],{"class":228},[176,12394,1054],{"class":182},[176,12396,4601],{"class":228},[176,12398,12399,12401,12403,12405,12408,12410,12413],{"class":178,"line":304},[176,12400,7751],{"class":228},[176,12402,4799],{"class":224},[176,12404,5850],{"class":228},[176,12406,12407],{"class":182},"Param",[176,12409,1237],{"class":228},[176,12411,12412],{"class":186},"\"filepath\"",[176,12414,1243],{"class":228},[176,12416,12417,12419,12421,12423,12425,12427,12429,12431,12434],{"class":178,"line":309},[176,12418,5865],{"class":224},[176,12420,7773],{"class":228},[176,12422,5871],{"class":224},[176,12424,12119],{"class":186},[176,12426,5964],{"class":224},[176,12428,7773],{"class":228},[176,12430,5871],{"class":224},[176,12432,12433],{"class":186}," \"\u002Findex.html\"",[176,12435,281],{"class":228},[176,12437,12438],{"class":178,"line":350},[176,12439,12440],{"class":247},"            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n",[176,12442,12443,12445,12448,12450,12453,12455,12458],{"class":178,"line":362},[176,12444,5880],{"class":228},[176,12446,12447],{"class":182},"Header",[176,12449,1237],{"class":228},[176,12451,12452],{"class":186},"\"Cache-Control\"",[176,12454,612],{"class":228},[176,12456,12457],{"class":186},"\"no-cache, no-store, must-revalidate\"",[176,12459,1243],{"class":228},[176,12461,12462,12465,12467,12470,12472,12475,12478,12481],{"class":178,"line":383},[176,12463,12464],{"class":228},"        } ",[176,12466,12139],{"class":224},[176,12468,12469],{"class":224}," if",[176,12471,5932],{"class":228},[176,12473,12474],{"class":182},"HasPrefix",[176,12476,12477],{"class":228},"(p, ",[176,12479,12480],{"class":186},"\"\u002Fassets\u002F\"",[176,12482,4601],{"class":228},[176,12484,12485],{"class":178,"line":389},[176,12486,12487],{"class":247},"            \u002F\u002F 带内容 hash 的资源可放心长缓存\n",[176,12489,12490,12492,12494,12496,12498,12500,12503],{"class":178,"line":395},[176,12491,5880],{"class":228},[176,12493,12447],{"class":182},[176,12495,1237],{"class":228},[176,12497,12452],{"class":186},[176,12499,612],{"class":228},[176,12501,12502],{"class":186},"\"public, max-age=31536000, immutable\"",[176,12504,1243],{"class":228},[176,12506,12507],{"class":178,"line":420},[176,12508,5918],{"class":228},[176,12510,12511,12514,12517,12519,12521,12524,12527],{"class":178,"line":426},[176,12512,12513],{"class":228},"        http.",[176,12515,12516],{"class":182},"StripPrefix",[176,12518,1237],{"class":228},[176,12520,12333],{"class":186},[176,12522,12523],{"class":228},", dist).",[176,12525,12526],{"class":182},"ServeHTTP",[176,12528,12529],{"class":228},"(c.Writer, c.Request)\n",[176,12531,12532],{"class":178,"line":750},[176,12533,5645],{"class":228},[176,12535,12536,12538,12540,12542,12544,12546,12548,12550,12552,12554,12556,12558,12560,12562,12564,12566,12568,12570,12572],{"class":178,"line":761},[176,12537,5551],{"class":228},[176,12539,5580],{"class":182},[176,12541,1237],{"class":228},[176,12543,7764],{"class":186},[176,12545,612],{"class":228},[176,12547,4973],{"class":224},[176,12549,1237],{"class":228},[176,12551,5594],{"class":287},[176,12553,5597],{"class":224},[176,12555,5600],{"class":182},[176,12557,408],{"class":228},[176,12559,1054],{"class":182},[176,12561,12352],{"class":228},[176,12563,12355],{"class":182},[176,12565,1237],{"class":228},[176,12567,12360],{"class":294},[176,12569,612],{"class":228},[176,12571,11769],{"class":186},[176,12573,12367],{"class":228},[176,12575,12576],{"class":178,"line":772},[176,12577,12307],{"class":224},[176,12579,12580],{"class":178,"line":778},[176,12581,301],{"class":228},[15,12583,12584],{},"缓存策略是内嵌发布的必修课（坑二细说）：入口禁缓存、hash 资源长缓存。",[162,12586,12588],{"id":12587},"_5-打包脚本windows-版-linux-交叉编译版","5. 打包脚本：Windows 版 + Linux 交叉编译版",[167,12590,12592],{"className":169,"code":12591,"language":171,"meta":172,"style":172},"#!\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",[36,12593,12594,12599,12604,12614,12632,12636,12657,12661,12669,12688,12692,12699,12756,12771,12775],{"__ignoreMap":172},[176,12595,12596],{"class":178,"line":179},[176,12597,12598],{"class":247},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[176,12600,12601],{"class":178,"line":244},[176,12602,12603],{"class":247},"# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\n",[176,12605,12606,12608,12611],{"class":178,"line":251},[176,12607,8029],{"class":294},[176,12609,12610],{"class":294}," -euo",[176,12612,12613],{"class":186}," pipefail\n",[176,12615,12616,12618,12621,12624,12626,12629],{"class":178,"line":259},[176,12617,8108],{"class":294},[176,12619,12620],{"class":186}," \"$(",[176,12622,12623],{"class":182},"dirname",[176,12625,1428],{"class":186},[176,12627,12628],{"class":294},"$0",[176,12630,12631],{"class":186},"\")\"\n",[176,12633,12634],{"class":178,"line":266},[176,12635,263],{"emptyLinePlaceholder":262},[176,12637,12638,12641,12643,12646,12648,12651,12654],{"class":178,"line":284},[176,12639,12640],{"class":228},"BINARY",[176,12642,326],{"class":224},[176,12644,12645],{"class":186},"\"${",[176,12647,12640],{"class":228},[176,12649,12650],{"class":224},":-",[176,12652,12653],{"class":228},"magic-img",[176,12655,12656],{"class":186},"}\"\n",[176,12658,12659],{"class":178,"line":298},[176,12660,263],{"emptyLinePlaceholder":262},[176,12662,12663,12666],{"class":178,"line":304},[176,12664,12665],{"class":294},"echo",[176,12667,12668],{"class":186}," \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n",[176,12670,12671,12673,12675,12678,12680,12682,12684,12686],{"class":178,"line":309},[176,12672,1237],{"class":228},[176,12674,8108],{"class":294},[176,12676,12677],{"class":186}," frontend",[176,12679,1466],{"class":228},[176,12681,183],{"class":182},[176,12683,1416],{"class":186},[176,12685,1537],{"class":186},[176,12687,1243],{"class":228},[176,12689,12690],{"class":178,"line":350},[176,12691,263],{"emptyLinePlaceholder":262},[176,12693,12694,12696],{"class":178,"line":362},[176,12695,12665],{"class":294},[176,12697,12698],{"class":186}," \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n",[176,12700,12701,12703,12705,12708,12711,12713,12715,12718,12720,12723,12726,12728,12731,12733,12735,12737,12739,12742,12745,12748,12751,12754],{"class":178,"line":383},[176,12702,1237],{"class":228},[176,12704,8108],{"class":294},[176,12706,12707],{"class":186}," backend",[176,12709,12710],{"class":228}," && CGO_ENABLED",[176,12712,326],{"class":224},[176,12714,552],{"class":186},[176,12716,12717],{"class":228}," GOOS",[176,12719,326],{"class":224},[176,12721,12722],{"class":186},"linux",[176,12724,12725],{"class":228}," GOARCH",[176,12727,326],{"class":224},[176,12729,12730],{"class":186},"amd64",[176,12732,1876],{"class":182},[176,12734,1537],{"class":186},[176,12736,1546],{"class":294},[176,12738,1428],{"class":186},[176,12740,12741],{"class":228},"$BINARY",[176,12743,12744],{"class":186},"\"",[176,12746,12747],{"class":294}," -ldflags=",[176,12749,12750],{"class":186},"\"-s -w\"",[176,12752,12753],{"class":186}," .\u002Fcmd\u002Fserver",[176,12755,1243],{"class":228},[176,12757,12758,12761,12764,12767,12769],{"class":178,"line":389},[176,12759,12760],{"class":182},"chmod",[176,12762,12763],{"class":186}," +x",[176,12765,12766],{"class":186}," \"backend\u002F",[176,12768,12741],{"class":228},[176,12770,1619],{"class":186},[176,12772,12773],{"class":178,"line":395},[176,12774,263],{"emptyLinePlaceholder":262},[176,12776,12777,12779,12782,12784],{"class":178,"line":420},[176,12778,12665],{"class":294},[176,12780,12781],{"class":186}," \"[3\u002F3] 打包完成: backend\u002F",[176,12783,12741],{"class":228},[176,12785,1619],{"class":186},[15,12787,12788,12789,12792,12793,910],{},"Windows 版（",[36,12790,12791],{},"build-win.bat","）同理，只是产物叫 ",[36,12794,12795],{},"magic-img.exe",[11,12797,8142],{"id":8142},[162,12799,12801,12802,12805,12806,12808],{"id":12800},"坑一goembed-dist-静默漏掉-_-开头文件升级解法","坑一：",[36,12803,12804],{},"\u002F\u002Fgo:embed dist"," 静默漏掉 ",[36,12807,8180],{}," 开头文件（升级解法）",[15,12810,12811,12812,12819,12820,12822],{},"上篇说过：embed 目录时",[25,12813,12814,12815,1193,12817,8181],{},"硬编码排除 ",[36,12816,8180],{},[36,12818,408],{},"。上篇的解法是改 Vite 输出文件名去掉 ",[36,12821,8180],{}," 前缀；这次直接用：",[167,12824,12826],{"className":4773,"code":12825,"language":1522,"meta":172,"style":172},"\u002F\u002Fgo:embed all:dist   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[36,12827,12828],{"__ignoreMap":172},[176,12829,12830,12833],{"class":178,"line":179},[176,12831,12832],{"class":247},"\u002F\u002Fgo:embed all:dist",[176,12834,12835],{"class":247},"   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[15,12837,12838],{},[25,12839,12840,12841,12843,12844,12846],{},"结论：能用 ",[36,12842,11701],{}," 就用 ",[36,12845,11701],{},"，从根上消灭这类\"文件莫名 404\"的问题。",[162,12848,12850],{"id":12849},"坑二开发版是新的打包版却是旧的","坑二：开发版是新的、打包版却是旧的",[15,12852,12853,12854,12857],{},"现象：Vite 开发服务一切正常，",[36,12855,12856],{},":8080\u002Fdist\u002F"," 打包版却是旧界面，强刷也不一定好。",[15,12859,12860,12861,12863,12864,12867,12868,12873],{},"排查过程值得记录：先是怀疑 ",[36,12862,8341],{}," 缓存了旧 embed（上篇坑三的思路），重编重启无效；然后直接抓取 8080 返回的 HTML 和 JS，发现",[25,12865,12866],{},"服务端内容已经是新的","——问题出在",[25,12869,12870,12871],{},"浏览器缓存了 ",[36,12872,11832],{},"：它引用旧 hash 的资源，一直不更新。",[15,12875,12876,12878,12879,12882,12883,12885],{},[25,12877,946],{},"：入口文件响应 ",[36,12880,12881],{},"Cache-Control: no-store","（见上面路由代码），带 hash 的资源给 ",[36,12884,1337],{}," 长缓存。升级后浏览器每次拉新入口，资源文件名一变就自动换新，两全其美。",[10490,12887,12888],{},[15,12889,12890],{},"教训：排查\"新旧不一致\"先分清是哪一层——服务端产物、浏览器入口缓存、还是资源缓存。抓包看响应内容再动手。",[162,12892,12894],{"id":12893},"坑三windows-上跑的-build-linuxsh产出的文件-linux-却执行不了","坑三：Windows 上跑的 build-linux.sh，产出的文件 Linux 却执行不了",[15,12896,12897,12898,7389,12901,12907,12908,12910],{},"最初脚本只写了 ",[36,12899,12900],{},"CGO_ENABLED=0 go build",[25,12902,12903,12904],{},"没指定 ",[36,12905,12906],{},"GOOS\u002FGOARCH","。在 Windows 上跑，产出的其实是个 Windows PE 文件（只是没叫 ",[36,12909,8643],{},"），传到 Linux 自然「无法执行」。",[15,12912,12913,12915],{},[25,12914,946],{},"：脚本里显式交叉编译：",[167,12917,12919],{"className":169,"code":12918,"language":171,"meta":172,"style":172},"CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver\n",[36,12920,12921],{"__ignoreMap":172},[176,12922,12923,12926,12928,12930,12932,12934,12936,12938,12940,12942,12944,12946,12948,12950,12952,12954,12956,12958],{"class":178,"line":179},[176,12924,12925],{"class":228},"CGO_ENABLED",[176,12927,326],{"class":224},[176,12929,552],{"class":186},[176,12931,12717],{"class":228},[176,12933,326],{"class":224},[176,12935,12722],{"class":186},[176,12937,12725],{"class":228},[176,12939,326],{"class":224},[176,12941,12730],{"class":186},[176,12943,1876],{"class":182},[176,12945,1537],{"class":186},[176,12947,1546],{"class":294},[176,12949,1428],{"class":186},[176,12951,12741],{"class":228},[176,12953,12744],{"class":186},[176,12955,12747],{"class":294},[176,12957,12750],{"class":186},[176,12959,12960],{"class":186}," .\u002Fcmd\u002Fserver\n",[15,12962,12963,12964,12967,12968,12971,12972],{},"能这么干的前提是",[25,12965,12966],{},"全依赖树无 cgo","——SQLite 用的是 modernc 纯 Go 驱动，bcrypt 用 ",[36,12969,12970],{},"x\u002Fcrypto","。这也是选型时就要留意的点：",[25,12973,12974],{},"想跨平台交叉编译，先把 cgo 依赖清干净。",[15,12976,12977,12978,142,12981,12984],{},"另外两个小提醒：Windows 上编出的文件没有 Unix 执行位，上传服务器后记得 ",[36,12979,12980],{},"chmod +x",[36,12982,12983],{},"-ldflags=\"-s -w\""," 剥离符号，体积从 30MB+ 降到 26MB。",[162,12986,12988],{"id":12987},"坑四dist-被-gitignore-后embed-直接编译失败","坑四：dist 被 gitignore 后，embed 直接编译失败",[15,12990,12991,12993,12994,12996,12997,13000,13001,13003],{},[36,12992,11857],{}," 是构建产物、入了 .gitignore，但 ",[36,12995,7544],{}," 在",[25,12998,12999],{},"编译期","就要读它——刚 clone 下来直接 ",[36,13002,8341],{}," 会报 embed 找不到文件。",[15,13005,13006,13008,13009,13012],{},[25,13007,946],{},"：gitignore 里保留 ",[36,13010,13011],{},"dist\u002F.gitkeep","（一个空文件），让目录存在于版本库中：",[167,13014,13018],{"className":13015,"code":13016,"language":13017,"meta":172,"style":172},"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",[36,13019,13020,13025,13030],{"__ignoreMap":172},[176,13021,13022],{"class":178,"line":179},[176,13023,13024],{},"backend\u002Fweb\u002Fdist\u002F*\n",[176,13026,13027],{"class":178,"line":244},[176,13028,13029],{},"!backend\u002Fweb\u002Fdist\u002F\n",[176,13031,13032],{"class":178,"line":251},[176,13033,13034],{},"!backend\u002Fweb\u002Fdist\u002F.gitkeep\n",[15,13036,13037,13038,13040,13041,13043],{},"这样 clone 后可以先 ",[36,13039,8341],{}," 通过，再 ",[36,13042,11850],{}," 灌入真实前端。",[162,13045,13047],{"id":13046},"坑五500kb-chunk-警告别慌","坑五：>500KB chunk 警告，别慌",[15,13049,13050,13051,13054,13055,910],{},"Vite 构建会警告单 chunk 超过 500KB。这个项目里 Base UI 组件库全量引入，单 chunk 约 640KB——",[25,13052,13053],{},"属正常，不要为了消警告去拆配置","。真要优化就上路由级 ",[36,13056,13057],{},"React.lazy",[11,13059,8354],{"id":8354},[167,13061,13063],{"className":169,"code":13062,"language":171,"meta":172,"style":172},"$ build-win.bat        # backendmagic-img.exe\n$ bash build-linux.sh  # backendmagic-img（linux\u002Famd64 ELF）\n",[36,13064,13065,13075],{"__ignoreMap":172},[176,13066,13067,13069,13072],{"class":178,"line":179},[176,13068,3479],{"class":182},[176,13070,13071],{"class":186}," build-win.bat",[176,13073,13074],{"class":247},"        # backendmagic-img.exe\n",[176,13076,13077,13079,13081,13084],{"class":178,"line":244},[176,13078,3479],{"class":182},[176,13080,8366],{"class":186},[176,13082,13083],{"class":186}," build-linux.sh",[176,13085,13086],{"class":247},"  # backendmagic-img（linux\u002Famd64 ELF）\n",[15,13088,13089,13090,13093,13094,910],{},"一个约 26MB 的二进制，内嵌 React 前端、Gin API、SQLite 数据初始化，拷到 Windows\u002FLinux 机器直接跑，浏览器访问 ",[36,13091,13092],{},"http:\u002F\u002Flocalhost:8080"," 自动重定向到 ",[36,13095,11836],{},[11,13097,2534],{"id":2534},[15,13099,13100],{},"这轮相比上篇的三点升级：",[19,13102,13103,13109,13119],{},[22,13104,13105,13108],{},[25,13106,13107],{},"hash 路由 + base \u002Fdist","：省掉 SPA fallback，静态服务即开即用；",[22,13110,13111,1032,13116,13118],{},[25,13112,13113,13115],{},[36,13114,11701],{}," embed 前缀",[36,13117,8180],{}," 开头文件问题一劳永逸；",[22,13120,13121,13127],{},[25,13122,13123,13124,13126],{},"显式 ",[36,13125,12906],{}," 交叉编译","：让「一个脚本、任意平台、产出 Linux 二进制」成为现实。",[15,13129,13130,13131,13134],{},"加上",[25,13132,13133],{},"入口 no-store \u002F 资源 immutable"," 的缓存策略，单文件分发的体验才算闭环——不然每次升级都要和浏览器缓存打架。",[1346,13136,13137],{},"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":172,"searchDepth":244,"depth":251,"links":13139},[13140,13141,13142,13149,13157,13158],{"id":3237,"depth":244,"text":3237},{"id":7538,"depth":244,"text":7538},{"id":7552,"depth":244,"text":7552,"children":13143},[13144,13145,13146,13147,13148],{"id":11729,"depth":251,"text":11730},{"id":11844,"depth":251,"text":11845},{"id":11900,"depth":251,"text":11901},{"id":12237,"depth":251,"text":12238},{"id":12587,"depth":251,"text":12588},{"id":8142,"depth":244,"text":8142,"children":13150},[13151,13153,13154,13155,13156],{"id":12800,"depth":251,"text":13152},"坑一：\u002F\u002Fgo:embed dist 静默漏掉 _ 开头文件（升级解法）",{"id":12849,"depth":251,"text":12850},{"id":12893,"depth":251,"text":12894},{"id":12987,"depth":251,"text":12988},{"id":13046,"depth":251,"text":13047},{"id":8354,"depth":244,"text":8354},{"id":2534,"depth":244,"text":2534},"以 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",{"title":11641,"description":13159},"posts\u002Fgo-embed-react-single-binary\u002Findex",[2179,1378,7592,13166,13167],"交叉编译","打包","WV-T-MOO5Cu-aycw-Ony8s9hjA3_k0S4XU-T0QsCow0",{"id":4,"title":5,"author":6,"body":13170,"description":1366,"difficulty":1367,"draft":1368,"extension":1369,"image":1370,"meta":14144,"navigation":262,"path":1372,"pinned":1368,"published":1373,"seo":14145,"series":1375,"seriesOrder":179,"stem":1376,"tags":14146,"updated":1382,"__hash__":1383},{"type":8,"value":13171,"toc":14128},[13172,13174,13176,13188,13192,13242,13248,13250,13266,13268,13270,13282,13284,13294,13438,13440,13766,13768,13790,13792,13794,13800,13804,13818,13822,13832,13836,13842,13844,13880,13888,14000,14008,14010,14014,14024,14028,14102,14106,14108,14114,14116,14118,14122,14126],[11,13173,13],{"id":13},[15,13175,17],{},[19,13177,13178,13182],{},[22,13179,13180,28],{},[25,13181,27],{},[22,13183,13184,34,13186,39],{},[25,13185,33],{},[36,13187,38],{},[15,13189,42,13190,46],{},[25,13191,45],{},[48,13193,13194,13206],{},[51,13195,13196],{},[54,13197,13198,13200,13202,13204],{},[57,13199,59],{},[57,13201,62],{},[57,13203,65],{},[57,13205,68],{},[70,13207,13208,13220,13230],{},[54,13209,13210,13214,13216,13218],{},[75,13211,77,13212,81],{},[36,13213,80],{},[75,13215,84],{},[75,13217,87],{},[75,13219,90],{},[54,13221,13222,13224,13226,13228],{},[75,13223,95],{},[75,13225,84],{},[75,13227,100],{},[75,13229,103],{},[54,13231,13232,13236,13238,13240],{},[75,13233,13234],{},[25,13235,110],{},[75,13237,113],{},[75,13239,116],{},[75,13241,119],{},[15,13243,122,13244,126,13246,130],{},[25,13245,125],{},[36,13247,129],{},[11,13249,133],{"id":133},[19,13251,13252,13256,13260,13262,13264],{},[22,13253,138,13254,142],{},[25,13255,141],{},[22,13257,145,13258,142],{},[25,13259,148],{},[22,13261,151],{},[22,13263,154],{},[22,13265,157],{},[11,13267,160],{"id":160},[162,13269,165],{"id":164},[167,13271,13272],{"className":169,"code":170,"language":171,"meta":172,"style":172},[36,13273,13274],{"__ignoreMap":172},[176,13275,13276,13278,13280],{"class":178,"line":179},[176,13277,183],{"class":182},[176,13279,187],{"class":186},[176,13281,190],{"class":186},[162,13283,194],{"id":193},[15,13285,197,13286,201,13288,205,13290,205,13292,212],{},[36,13287,200],{},[36,13289,204],{},[36,13291,208],{},[36,13293,211],{},[167,13295,13296],{"className":215,"code":216,"language":217,"meta":172,"style":172},[36,13297,13298,13312,13316,13322,13326,13338,13346,13350,13354,13382,13390,13404,13408,13412,13430,13434],{"__ignoreMap":172},[176,13299,13300,13302,13304,13306,13308,13310],{"class":178,"line":179},[176,13301,225],{"class":224},[176,13303,229],{"class":228},[176,13305,232],{"class":224},[176,13307,235],{"class":228},[176,13309,238],{"class":224},[176,13311,241],{"class":186},[176,13313,13314],{"class":178,"line":244},[176,13315,248],{"class":247},[176,13317,13318,13320],{"class":178,"line":251},[176,13319,225],{"class":224},[176,13321,256],{"class":186},[176,13323,13324],{"class":178,"line":259},[176,13325,263],{"emptyLinePlaceholder":262},[176,13327,13328,13330,13332,13334,13336],{"class":178,"line":266},[176,13329,269],{"class":224},[176,13331,272],{"class":182},[176,13333,275],{"class":224},[176,13335,278],{"class":182},[176,13337,281],{"class":228},[176,13339,13340,13342,13344],{"class":178,"line":284},[176,13341,288],{"class":287},[176,13343,291],{"class":224},[176,13345,295],{"class":294},[176,13347,13348],{"class":178,"line":298},[176,13349,301],{"class":228},[176,13351,13352],{"class":178,"line":304},[176,13353,263],{"emptyLinePlaceholder":262},[176,13355,13356,13358,13360,13362,13364,13366,13368,13370,13372,13374,13376,13378,13380],{"class":178,"line":309},[176,13357,312],{"class":224},[176,13359,315],{"class":294},[176,13361,318],{"class":224},[176,13363,272],{"class":182},[176,13365,323],{"class":228},[176,13367,326],{"class":224},[176,13369,329],{"class":228},[176,13371,332],{"class":182},[176,13373,335],{"class":228},[176,13375,338],{"class":287},[176,13377,341],{"class":228},[176,13379,344],{"class":224},[176,13381,347],{"class":228},[176,13383,13384,13386,13388],{"class":178,"line":350},[176,13385,353],{"class":228},[176,13387,356],{"class":182},[176,13389,359],{"class":228},[176,13391,13392,13394,13396,13398,13400,13402],{"class":178,"line":362},[176,13393,365],{"class":228},[176,13395,368],{"class":182},[176,13397,371],{"class":228},[176,13399,374],{"class":224},[176,13401,377],{"class":182},[176,13403,380],{"class":228},[176,13405,13406],{"class":178,"line":383},[176,13407,386],{"class":228},[176,13409,13410],{"class":178,"line":389},[176,13411,392],{"class":228},[176,13413,13414,13416,13418,13420,13422,13424,13426,13428],{"class":178,"line":395},[176,13415,398],{"class":228},[176,13417,374],{"class":224},[176,13419,403],{"class":186},[176,13421,338],{"class":228},[176,13423,408],{"class":186},[176,13425,411],{"class":228},[176,13427,414],{"class":186},[176,13429,417],{"class":228},[176,13431,13432],{"class":178,"line":420},[176,13433,423],{"class":228},[176,13435,13436],{"class":178,"line":426},[176,13437,429],{"class":228},[162,13439,433],{"id":432},[167,13441,13442],{"className":215,"code":436,"language":217,"meta":172,"style":172},[36,13443,13444,13450,13458,13512,13524,13536,13544,13562,13570,13588,13602,13628,13634,13660,13666,13674,13688,13696,13704,13708,13716,13730,13734,13742,13750,13754,13758,13762],{"__ignoreMap":172},[176,13445,13446,13448],{"class":178,"line":179},[176,13447,443],{"class":228},[176,13449,446],{"class":294},[176,13451,13452,13454,13456],{"class":178,"line":244},[176,13453,451],{"class":182},[176,13455,326],{"class":224},[176,13457,456],{"class":186},[176,13459,13460,13462,13464,13466,13468,13470,13472,13474,13476,13478,13480,13482,13484,13486,13488,13490,13492,13494,13496,13498,13500,13502,13504,13506,13508,13510],{"class":178,"line":251},[176,13461,461],{"class":182},[176,13463,326],{"class":224},[176,13465,466],{"class":228},[176,13467,469],{"class":287},[176,13469,341],{"class":228},[176,13471,344],{"class":224},[176,13473,476],{"class":228},[176,13475,443],{"class":224},[176,13477,481],{"class":294},[176,13479,484],{"class":224},[176,13481,487],{"class":294},[176,13483,490],{"class":224},[176,13485,493],{"class":228},[176,13487,443],{"class":224},[176,13489,498],{"class":294},[176,13491,484],{"class":224},[176,13493,503],{"class":294},[176,13495,490],{"class":224},[176,13497,493],{"class":228},[176,13499,443],{"class":224},[176,13501,512],{"class":294},[176,13503,484],{"class":224},[176,13505,517],{"class":294},[176,13507,490],{"class":224},[176,13509,522],{"class":294},[176,13511,525],{"class":228},[176,13513,13514,13516,13518,13520,13522],{"class":178,"line":259},[176,13515,530],{"class":182},[176,13517,326],{"class":224},[176,13519,535],{"class":228},[176,13521,538],{"class":294},[176,13523,301],{"class":228},[176,13525,13526,13528,13530,13532,13534],{"class":178,"line":266},[176,13527,545],{"class":182},[176,13529,326],{"class":224},[176,13531,535],{"class":228},[176,13533,552],{"class":294},[176,13535,301],{"class":228},[176,13537,13538,13540,13542],{"class":178,"line":284},[176,13539,559],{"class":182},[176,13541,326],{"class":224},[176,13543,564],{"class":228},[176,13545,13546,13548,13550,13552,13554,13556,13558,13560],{"class":178,"line":298},[176,13547,569],{"class":182},[176,13549,326],{"class":224},[176,13551,574],{"class":228},[176,13553,577],{"class":287},[176,13555,580],{"class":228},[176,13557,344],{"class":224},[176,13559,585],{"class":182},[176,13561,588],{"class":228},[176,13563,13564,13566,13568],{"class":178,"line":304},[176,13565,593],{"class":182},[176,13567,326],{"class":224},[176,13569,598],{"class":228},[176,13571,13572,13574,13576,13578,13580,13582,13584,13586],{"class":178,"line":309},[176,13573,603],{"class":182},[176,13575,606],{"class":228},[176,13577,609],{"class":287},[176,13579,612],{"class":228},[176,13581,615],{"class":287},[176,13583,341],{"class":228},[176,13585,344],{"class":224},[176,13587,281],{"class":228},[176,13589,13590,13592,13594,13596,13598,13600],{"class":178,"line":350},[176,13591,626],{"class":224},[176,13593,629],{"class":228},[176,13595,632],{"class":294},[176,13597,635],{"class":228},[176,13599,326],{"class":224},[176,13601,640],{"class":228},[176,13603,13604,13606,13608,13610,13612,13614,13616,13618,13620,13622,13624,13626],{"class":178,"line":362},[176,13605,626],{"class":224},[176,13607,629],{"class":228},[176,13609,649],{"class":294},[176,13611,612],{"class":228},[176,13613,577],{"class":294},[176,13615,612],{"class":228},[176,13617,208],{"class":294},[176,13619,612],{"class":228},[176,13621,211],{"class":294},[176,13623,635],{"class":228},[176,13625,326],{"class":224},[176,13627,668],{"class":228},[176,13629,13630,13632],{"class":178,"line":383},[176,13631,673],{"class":224},[176,13633,676],{"class":228},[176,13635,13636,13638,13640,13642,13644,13646,13648,13650,13652,13654,13656,13658],{"class":178,"line":389},[176,13637,681],{"class":228},[176,13639,685],{"class":684},[176,13641,688],{"class":182},[176,13643,326],{"class":224},[176,13645,693],{"class":186},[176,13647,696],{"class":182},[176,13649,326],{"class":224},[176,13651,701],{"class":228},[176,13653,704],{"class":182},[176,13655,326],{"class":224},[176,13657,709],{"class":186},[176,13659,712],{"class":228},[176,13661,13662,13664],{"class":178,"line":395},[176,13663,717],{"class":228},[176,13665,720],{"class":684},[176,13667,13668,13670,13672],{"class":178,"line":420},[176,13669,725],{"class":182},[176,13671,326],{"class":224},[176,13673,730],{"class":228},[176,13675,13676,13678,13680,13682,13684,13686],{"class":178,"line":426},[176,13677,735],{"class":182},[176,13679,326],{"class":224},[176,13681,740],{"class":228},[176,13683,374],{"class":224},[176,13685,745],{"class":186},[176,13687,301],{"class":228},[176,13689,13690,13692,13694],{"class":178,"line":750},[176,13691,753],{"class":182},[176,13693,326],{"class":224},[176,13695,758],{"class":186},[176,13697,13698,13700,13702],{"class":178,"line":761},[176,13699,764],{"class":182},[176,13701,326],{"class":224},[176,13703,769],{"class":228},[176,13705,13706],{"class":178,"line":772},[176,13707,775],{"class":228},[176,13709,13710,13712,13714],{"class":178,"line":778},[176,13711,781],{"class":228},[176,13713,784],{"class":247},[176,13715,301],{"class":228},[176,13717,13718,13720,13722,13724,13726,13728],{"class":178,"line":789},[176,13719,717],{"class":228},[176,13721,794],{"class":684},[176,13723,797],{"class":182},[176,13725,326],{"class":224},[176,13727,802],{"class":186},[176,13729,712],{"class":228},[176,13731,13732],{"class":178,"line":807},[176,13733,810],{"class":228},[176,13735,13736,13738,13740],{"class":178,"line":813},[176,13737,816],{"class":228},[176,13739,794],{"class":684},[176,13741,712],{"class":228},[176,13743,13744,13746,13748],{"class":178,"line":823},[176,13745,826],{"class":228},[176,13747,685],{"class":684},[176,13749,712],{"class":228},[176,13751,13752],{"class":178,"line":833},[176,13753,836],{"class":228},[176,13755,13756],{"class":178,"line":839},[176,13757,842],{"class":228},[176,13759,13760],{"class":178,"line":845},[176,13761,848],{"class":228},[176,13763,13764],{"class":178,"line":851},[176,13765,854],{"class":228},[15,13767,857],{},[859,13769,13770,13776,13784],{},[22,13771,13772,865,13774,869],{},[36,13773,80],{},[36,13775,868],{},[22,13777,872,13778,875,13780,879,13782,882],{},[36,13779,632],{},[25,13781,878],{},[36,13783,577],{},[22,13785,885,13786,889,13788,892],{},[36,13787,888],{},[36,13789,129],{},[11,13791,895],{"id":895},[162,13793,899],{"id":898},[15,13795,902,13796,906,13798,910],{},[36,13797,905],{},[25,13799,909],{},[15,13801,913,13802,916],{},[36,13803,80],{},[15,13805,919,13806,923,13808,205,13810,930,13812,937,13816,941],{},[36,13807,922],{},[36,13809,926],{},[36,13811,929],{},[25,13813,933,13814],{},[36,13815,936],{},[36,13817,940],{},[15,13819,13820,947],{},[25,13821,946],{},[167,13823,13824],{"className":215,"code":950,"language":217,"meta":172,"style":172},[36,13825,13826],{"__ignoreMap":172},[176,13827,13828,13830],{"class":178,"line":179},[176,13829,225],{"class":224},[176,13831,256],{"class":186},[15,13833,961,13834],{},[25,13835,964],{},[162,13837,968,13838,971,13840,975],{"id":967},[36,13839,888],{},[36,13841,974],{},[15,13843,978],{},[167,13845,13846],{"className":215,"code":981,"language":217,"meta":172,"style":172},[36,13847,13848],{"__ignoreMap":172},[176,13849,13850,13852,13854,13856,13858,13860,13862,13864,13866,13868,13870,13872,13874,13876,13878],{"class":178,"line":179},[176,13851,649],{"class":182},[176,13853,990],{"class":228},[176,13855,649],{"class":287},[176,13857,612],{"class":228},[176,13859,577],{"class":287},[176,13861,612],{"class":228},[176,13863,208],{"class":287},[176,13865,612],{"class":228},[176,13867,211],{"class":287},[176,13869,612],{"class":228},[176,13871,632],{"class":287},[176,13873,580],{"class":228},[176,13875,344],{"class":224},[176,13877,1015],{"class":224},[176,13879,1018],{"class":247},[15,13881,1021,13882,1024,13884,1028,13886,1032],{},[36,13883,649],{},[36,13885,1027],{},[25,13887,1031],{},[167,13889,13890],{"className":215,"code":1035,"language":217,"meta":172,"style":172},[36,13891,13892,13932,13936,13956,13968,13992,13996],{"__ignoreMap":172},[176,13893,13894,13896,13898,13900,13902,13904,13906,13908,13910,13912,13914,13916,13918,13920,13922,13924,13926,13928,13930],{"class":178,"line":179},[176,13895,232],{"class":224},[176,13897,1044],{"class":182},[176,13899,443],{"class":228},[176,13901,1049],{"class":182},[176,13903,612],{"class":228},[176,13905,1054],{"class":182},[176,13907,1057],{"class":228},[176,13909,326],{"class":224},[176,13911,1062],{"class":228},[176,13913,609],{"class":287},[176,13915,318],{"class":224},[176,13917,1069],{"class":182},[176,13919,612],{"class":228},[176,13921,615],{"class":287},[176,13923,318],{"class":224},[176,13925,1078],{"class":182},[176,13927,341],{"class":228},[176,13929,344],{"class":224},[176,13931,1085],{"class":182},[176,13933,13934],{"class":178,"line":244},[176,13935,1090],{"class":247},[176,13937,13938,13940,13942,13944,13946,13948,13950,13952,13954],{"class":178,"line":251},[176,13939,649],{"class":182},[176,13941,318],{"class":224},[176,13943,1062],{"class":228},[176,13945,609],{"class":287},[176,13947,612],{"class":228},[176,13949,615],{"class":287},[176,13951,341],{"class":228},[176,13953,344],{"class":224},[176,13955,281],{"class":228},[176,13957,13958,13960,13962,13964,13966],{"class":178,"line":259},[176,13959,1115],{"class":182},[176,13961,629],{"class":228},[176,13963,632],{"class":287},[176,13965,1122],{"class":228},[176,13967,1125],{"class":287},[176,13969,13970,13972,13974,13976,13978,13980,13982,13984,13986,13988,13990],{"class":178,"line":266},[176,13971,1115],{"class":182},[176,13973,629],{"class":228},[176,13975,649],{"class":287},[176,13977,612],{"class":228},[176,13979,577],{"class":287},[176,13981,612],{"class":228},[176,13983,208],{"class":287},[176,13985,612],{"class":228},[176,13987,211],{"class":287},[176,13989,1122],{"class":228},[176,13991,1150],{"class":287},[176,13993,13994],{"class":178,"line":284},[176,13995,1155],{"class":224},[176,13997,13998],{"class":178,"line":298},[176,13999,301],{"class":228},[15,14001,14002,1164,14004,1167,14006,1171],{},[25,14003,946],{},[36,14005,974],{},[36,14007,1170],{},[162,14009,1175],{"id":1174},[15,14011,1178,14012,1182],{},[36,14013,1181],{},[15,14015,1185,14016,1189,14018,1193,14020,1197,14022,1201],{},[25,14017,1188],{},[36,14019,1192],{},[36,14021,1196],{},[25,14023,1200],{},[15,14025,14026,1206],{},[25,14027,946],{},[167,14029,14030],{"className":1209,"code":1210,"language":1211,"meta":172,"style":172},[36,14031,14032,14054,14076],{"__ignoreMap":172},[176,14033,14034,14036,14038,14040,14042,14044,14046,14048,14050,14052],{"class":178,"line":179},[176,14035,312],{"class":224},[176,14037,1220],{"class":294},[176,14039,1223],{"class":224},[176,14041,1226],{"class":228},[176,14043,552],{"class":294},[176,14045,1231],{"class":228},[176,14047,1234],{"class":182},[176,14049,1237],{"class":228},[176,14051,1240],{"class":186},[176,14053,1243],{"class":228},[176,14055,14056,14058,14060,14062,14064,14066,14068,14070,14072,14074],{"class":178,"line":244},[176,14057,312],{"class":224},[176,14059,1250],{"class":294},[176,14061,1223],{"class":224},[176,14063,1226],{"class":228},[176,14065,1257],{"class":294},[176,14067,1231],{"class":228},[176,14069,1234],{"class":182},[176,14071,1237],{"class":228},[176,14073,1240],{"class":186},[176,14075,1243],{"class":228},[176,14077,14078,14080,14082,14084,14086,14088,14090,14092,14094,14096,14098,14100],{"class":178,"line":251},[176,14079,1272],{"class":224},[176,14081,1275],{"class":228},[176,14083,1278],{"class":182},[176,14085,1281],{"class":228},[176,14087,1284],{"class":182},[176,14089,1287],{"class":228},[176,14091,1290],{"class":224},[176,14093,1293],{"class":228},[176,14095,1284],{"class":182},[176,14097,1298],{"class":228},[176,14099,1301],{"class":224},[176,14101,1304],{"class":294},[15,14103,961,14104,1310],{},[25,14105,1309],{},[162,14107,1314],{"id":1313},[15,14109,1317,14110,1321,14112,1325],{},[36,14111,1320],{},[36,14113,1324],{},[15,14115,1328],{},[11,14117,1331],{"id":1331},[15,14119,1334,14120,1338],{},[36,14121,1337],{},[15,14123,1341,14124],{},[25,14125,1344],{},[1346,14127,1348],{},{"title":172,"searchDepth":244,"depth":251,"links":14129},[14130,14131,14132,14137,14143],{"id":13,"depth":244,"text":13},{"id":133,"depth":244,"text":133},{"id":160,"depth":244,"text":160,"children":14133},[14134,14135,14136],{"id":164,"depth":251,"text":165},{"id":193,"depth":251,"text":194},{"id":432,"depth":251,"text":433},{"id":895,"depth":244,"text":895,"children":14138},[14139,14140,14141,14142],{"id":898,"depth":251,"text":899},{"id":967,"depth":251,"text":1362},{"id":1174,"depth":251,"text":1175},{"id":1313,"depth":251,"text":1314},{"id":1331,"depth":244,"text":1331},{},{"title":5,"description":1366},[1378,110,1379,1380,1381],1786887348660]