[{"data":1,"prerenderedAt":9423},["ShallowReactive",2],{"post-go-embed-frontend":3,"series-source-go-embed-frontend":1104},{"id":4,"title":5,"author":6,"body":7,"description":1086,"difficulty":1087,"draft":1088,"extension":1089,"image":1090,"meta":1091,"navigation":82,"path":1092,"pinned":1088,"published":1093,"seo":1094,"series":1095,"seriesOrder":68,"stem":1096,"tags":1097,"updated":1102,"__hash__":1103},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",null,{"type":8,"value":9,"toc":1069},"minimark",[10,14,23,26,29,40,43,48,458,462,573,577,718,721,725,734,752,763,783,797,803,811,919,925,929,936,945,948,986,989,994,1039,1042,1048,1062,1065],[11,12,13],"h2",{"id":13},"背景",[15,16,17,18,22],"p",{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[19,20,21],"code",{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[15,24,25],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[11,27,28],{"id":28},"核心思路",[15,30,31,32,35,36,39],{},"利用 Go 1.16 引入的 ",[19,33,34],{},"\u002F\u002Fgo:embed"," 指令，在编译时将前端 ",[19,37,38],{},"dist\u002F"," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[11,41,42],{"id":42},"逐步实现",[44,45,47],"h3",{"id":46},"_1-创建嵌入模块","1. 创建嵌入模块",[49,50,55],"pre",{"className":51,"code":52,"language":53,"meta":54,"style":54},"language-go shiki shiki-themes github-light github-dark","\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","go","",[19,56,57,66,77,84,94,107,117,127,137,147,157,162,172,178,183,189,206,211,234,257,262,291,313,330,342,348,353,369,386,392,412,417,422,446,452],{"__ignoreMap":54},[58,59,62],"span",{"class":60,"line":61},"line",1,[58,63,65],{"class":64},"sHbNN","\u002F\u002F service\u002Fweb\u002Fembed.go\n",[58,67,69,73],{"class":60,"line":68},2,[58,70,72],{"class":71},"siTax","package",[58,74,76],{"class":75},"sw2iP"," web\n",[58,78,80],{"class":60,"line":79},3,[58,81,83],{"emptyLinePlaceholder":82},true,"\n",[58,85,87,90],{"class":60,"line":86},4,[58,88,89],{"class":71},"import",[58,91,93],{"class":92},"sIX_F"," (\n",[58,95,97,101,104],{"class":60,"line":96},5,[58,98,100],{"class":99},"scXbn","    \"",[58,102,103],{"class":75},"embed",[58,105,106],{"class":99},"\"\n",[58,108,110,112,115],{"class":60,"line":109},6,[58,111,100],{"class":99},[58,113,114],{"class":75},"io\u002Ffs",[58,116,106],{"class":99},[58,118,120,122,125],{"class":60,"line":119},7,[58,121,100],{"class":99},[58,123,124],{"class":75},"mime",[58,126,106],{"class":99},[58,128,130,132,135],{"class":60,"line":129},8,[58,131,100],{"class":99},[58,133,134],{"class":75},"net\u002Fhttp",[58,136,106],{"class":99},[58,138,140,142,145],{"class":60,"line":139},9,[58,141,100],{"class":99},[58,143,144],{"class":75},"path\u002Ffilepath",[58,146,106],{"class":99},[58,148,150,152,155],{"class":60,"line":149},10,[58,151,100],{"class":99},[58,153,154],{"class":75},"strings",[58,156,106],{"class":99},[58,158,160],{"class":60,"line":159},11,[58,161,83],{"emptyLinePlaceholder":82},[58,163,165,167,170],{"class":60,"line":164},12,[58,166,100],{"class":99},[58,168,169],{"class":75},"github.com\u002Fgin-gonic\u002Fgin",[58,171,106],{"class":99},[58,173,175],{"class":60,"line":174},13,[58,176,177],{"class":92},")\n",[58,179,181],{"class":60,"line":180},14,[58,182,83],{"emptyLinePlaceholder":82},[58,184,186],{"class":60,"line":185},15,[58,187,188],{"class":64},"\u002F\u002Fgo:embed dist\n",[58,190,192,195,198,200,203],{"class":60,"line":191},16,[58,193,194],{"class":71},"var",[58,196,197],{"class":92}," embedFS ",[58,199,103],{"class":75},[58,201,202],{"class":92},".",[58,204,205],{"class":75},"FS\n",[58,207,209],{"class":60,"line":208},17,[58,210,83],{"emptyLinePlaceholder":82},[58,212,214,217,220,223,226,228,231],{"class":60,"line":213},18,[58,215,216],{"class":71},"func",[58,218,219],{"class":75}," SPAHandler",[58,221,222],{"class":92},"() ",[58,224,225],{"class":75},"gin",[58,227,202],{"class":92},[58,229,230],{"class":75},"HandlerFunc",[58,232,233],{"class":92}," {\n",[58,235,237,240,243,246,249,252,255],{"class":60,"line":236},19,[58,238,239],{"class":92},"    sub, _ ",[58,241,242],{"class":71},":=",[58,244,245],{"class":92}," fs.",[58,247,248],{"class":75},"Sub",[58,250,251],{"class":92},"(embedFS, ",[58,253,254],{"class":99},"\"dist\"",[58,256,177],{"class":92},[58,258,260],{"class":60,"line":259},20,[58,261,83],{"emptyLinePlaceholder":82},[58,263,265,268,271,274,278,281,283,285,288],{"class":60,"line":264},21,[58,266,267],{"class":71},"    return",[58,269,270],{"class":71}," func",[58,272,273],{"class":92},"(",[58,275,277],{"class":276},"sPK6S","c",[58,279,280],{"class":71}," *",[58,282,225],{"class":75},[58,284,202],{"class":92},[58,286,287],{"class":75},"Context",[58,289,290],{"class":92},") {\n",[58,292,294,297,299,302,305,308,311],{"class":60,"line":293},22,[58,295,296],{"class":92},"        p ",[58,298,242],{"class":71},[58,300,301],{"class":92}," strings.",[58,303,304],{"class":75},"TrimPrefix",[58,306,307],{"class":92},"(c.Request.URL.Path, ",[58,309,310],{"class":99},"\"\u002F\"",[58,312,177],{"class":92},[58,314,316,319,322,325,328],{"class":60,"line":315},23,[58,317,318],{"class":71},"        if",[58,320,321],{"class":92}," p ",[58,323,324],{"class":71},"==",[58,326,327],{"class":99}," \"\"",[58,329,233],{"class":92},[58,331,333,336,339],{"class":60,"line":332},24,[58,334,335],{"class":92},"            p ",[58,337,338],{"class":71},"=",[58,340,341],{"class":99}," \"index.html\"\n",[58,343,345],{"class":60,"line":344},25,[58,346,347],{"class":92},"        }\n",[58,349,351],{"class":60,"line":350},26,[58,352,83],{"emptyLinePlaceholder":82},[58,354,356,359,361,363,366],{"class":60,"line":355},27,[58,357,358],{"class":92},"        data, err ",[58,360,242],{"class":71},[58,362,245],{"class":92},[58,364,365],{"class":75},"ReadFile",[58,367,368],{"class":92},"(sub, p)\n",[58,370,372,374,377,380,384],{"class":60,"line":371},28,[58,373,318],{"class":71},[58,375,376],{"class":92}," err ",[58,378,379],{"class":71},"!=",[58,381,383],{"class":382},"suQ91"," nil",[58,385,233],{"class":92},[58,387,389],{"class":60,"line":388},29,[58,390,391],{"class":64},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[58,393,395,398,400,402,404,407,410],{"class":60,"line":394},30,[58,396,397],{"class":92},"            data, _ ",[58,399,338],{"class":71},[58,401,245],{"class":92},[58,403,365],{"class":75},[58,405,406],{"class":92},"(sub, ",[58,408,409],{"class":99},"\"index.html\"",[58,411,177],{"class":92},[58,413,415],{"class":60,"line":414},31,[58,416,347],{"class":92},[58,418,420],{"class":60,"line":419},32,[58,421,83],{"emptyLinePlaceholder":82},[58,423,425,428,431,434,437,440,443],{"class":60,"line":424},33,[58,426,427],{"class":92},"        c.",[58,429,430],{"class":75},"Data",[58,432,433],{"class":92},"(http.StatusOK, mime.",[58,435,436],{"class":75},"TypeByExtension",[58,438,439],{"class":92},"(filepath.",[58,441,442],{"class":75},"Ext",[58,444,445],{"class":92},"(p)), data)\n",[58,447,449],{"class":60,"line":448},34,[58,450,451],{"class":92},"    }\n",[58,453,455],{"class":60,"line":454},35,[58,456,457],{"class":92},"}\n",[44,459,461],{"id":460},"_2-改造路由","2. 改造路由",[49,463,465],{"className":51,"code":464,"language":53,"meta":54,"style":54},"\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",[19,466,467,472,484,488,493,509,524,537,550,563],{"__ignoreMap":54},[58,468,469],{"class":60,"line":61},[58,470,471],{"class":64},"\u002F\u002F router.go\n",[58,473,474,476,479,482],{"class":60,"line":68},[58,475,89],{"class":71},[58,477,478],{"class":99}," \"",[58,480,481],{"class":75},"myproject\u002Fweb",[58,483,106],{"class":99},[58,485,486],{"class":60,"line":79},[58,487,83],{"emptyLinePlaceholder":82},[58,489,490],{"class":60,"line":86},[58,491,492],{"class":64},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[58,494,495,498,500,503,506],{"class":60,"line":96},[58,496,497],{"class":92},"webHandler ",[58,499,242],{"class":71},[58,501,502],{"class":92}," web.",[58,504,505],{"class":75},"SPAHandler",[58,507,508],{"class":92},"()\n",[58,510,511,514,517,519,521],{"class":60,"line":109},[58,512,513],{"class":92},"router.",[58,515,516],{"class":75},"GET",[58,518,273],{"class":92},[58,520,310],{"class":99},[58,522,523],{"class":92},", webHandler)\n",[58,525,526,528,530,532,535],{"class":60,"line":119},[58,527,513],{"class":92},[58,529,516],{"class":75},[58,531,273],{"class":92},[58,533,534],{"class":99},"\"\u002Fassets\u002F*filepath\"",[58,536,523],{"class":92},[58,538,539,541,543,545,548],{"class":60,"line":129},[58,540,513],{"class":92},[58,542,516],{"class":75},[58,544,273],{"class":92},[58,546,547],{"class":99},"\"\u002Ffavicon.ico\"",[58,549,523],{"class":92},[58,551,552,554,556,558,561],{"class":60,"line":139},[58,553,513],{"class":92},[58,555,516],{"class":75},[58,557,273],{"class":92},[58,559,560],{"class":99},"\"\u002Ffavicon.svg\"",[58,562,523],{"class":92},[58,564,565,567,570],{"class":60,"line":149},[58,566,513],{"class":92},[58,568,569],{"class":75},"NoRoute",[58,571,572],{"class":92},"(webHandler)\n",[44,574,576],{"id":575},"_3-一键构建脚本","3. 一键构建脚本",[49,578,582],{"className":579,"code":580,"language":581,"meta":54,"style":54},"language-bash shiki shiki-themes github-light github-dark","#!\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","bash",[19,583,584,589,594,598,606,610,615,626,637,641,646,657,670,674,679,687,703,713],{"__ignoreMap":54},[58,585,586],{"class":60,"line":61},[58,587,588],{"class":64},"#!\u002Fbin\u002Fbash\n",[58,590,591],{"class":60,"line":68},[58,592,593],{"class":64},"# build-standalone.sh\n",[58,595,596],{"class":60,"line":79},[58,597,83],{"emptyLinePlaceholder":82},[58,599,600,603],{"class":60,"line":86},[58,601,602],{"class":382},"set",[58,604,605],{"class":382}," -e\n",[58,607,608],{"class":60,"line":96},[58,609,83],{"emptyLinePlaceholder":82},[58,611,612],{"class":60,"line":109},[58,613,614],{"class":64},"# 1. 构建前端\n",[58,616,617,620,623],{"class":60,"line":119},[58,618,619],{"class":75},"pnpm",[58,621,622],{"class":99}," install",[58,624,625],{"class":382}," --no-frozen-lockfile\n",[58,627,628,631,634],{"class":60,"line":129},[58,629,630],{"class":75},"npx",[58,632,633],{"class":99}," vite",[58,635,636],{"class":99}," build\n",[58,638,639],{"class":60,"line":139},[58,640,83],{"emptyLinePlaceholder":82},[58,642,643],{"class":60,"line":149},[58,644,645],{"class":64},"# 2. 复制到嵌入目录\n",[58,647,648,651,654],{"class":60,"line":159},[58,649,650],{"class":75},"rm",[58,652,653],{"class":382}," -rf",[58,655,656],{"class":99}," service\u002Fweb\u002Fdist\n",[58,658,659,662,665,668],{"class":60,"line":164},[58,660,661],{"class":75},"cp",[58,663,664],{"class":382}," -r",[58,666,667],{"class":99}," dist",[58,669,656],{"class":99},[58,671,672],{"class":60,"line":174},[58,673,83],{"emptyLinePlaceholder":82},[58,675,676],{"class":60,"line":180},[58,677,678],{"class":64},"# 3. 编译 Go 二进制\n",[58,680,681,684],{"class":60,"line":185},[58,682,683],{"class":382},"cd",[58,685,686],{"class":99}," service\n",[58,688,689,691,694,697,700],{"class":60,"line":191},[58,690,53],{"class":75},[58,692,693],{"class":99}," build",[58,695,696],{"class":382}," -o",[58,698,699],{"class":99}," myapp.exe",[58,701,702],{"class":382}," \\\n",[58,704,705,708,711],{"class":60,"line":208},[58,706,707],{"class":382},"    -ldflags=",[58,709,710],{"class":99},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[58,712,702],{"class":382},[58,714,715],{"class":60,"line":213},[58,716,717],{"class":99},"    main.go\n",[11,719,720],{"id":720},"踩坑记录",[44,722,724],{"id":723},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[15,726,727,728,730,731,733],{},"首次实现时用了 Gin 的 ",[19,729,569],{}," 兜底，但部分静态资源返回 404。排查发现 ",[19,732,569],{}," 在某些路由未显式注册时不会按预期触发。",[15,735,736,740,741,744,745,748,749,751],{},[737,738,739],"strong",{},"解决","：显式注册 ",[19,742,743],{},"\u002Fassets\u002F*filepath","、",[19,746,747],{},"\u002Ffavicon.ico"," 等路由，",[19,750,569],{}," 仅作为兜底。",[44,753,755,756,758,759,762],{"id":754},"坑二goembed-排除-_-开头文件","坑二：",[19,757,34],{}," 排除 ",[19,760,761],{},"_"," 开头文件",[15,764,765,766,769,770,772,773,782],{},"这是最坑的。Vite 构建产出了一个 ",[19,767,768],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[19,771,34],{}," 当模式为目录时，",[737,774,775,776,778,779,781],{},"硬编码排除了 ",[19,777,761],{}," 和 ",[19,780,202],{}," 开头的文件","。",[49,784,786],{"className":51,"code":785,"language":53,"meta":54,"style":54},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[19,787,788,793],{"__ignoreMap":54},[58,789,790],{"class":60,"line":61},[58,791,792],{"class":64},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[58,794,795],{"class":60,"line":68},[58,796,188],{"class":64},[15,798,799,800,782],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[19,801,802],{},"Unexpected token '\u003C'",[15,804,805,807,808,810],{},[737,806,739],{},"：改 Vite 配置，去除了 ",[19,809,761],{}," 前缀：",[49,812,816],{"className":813,"code":814,"language":815,"meta":54,"style":54},"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",[19,817,818,823,831,838,845,853,899,904,909,914],{"__ignoreMap":54},[58,819,820],{"class":60,"line":61},[58,821,822],{"class":64},"\u002F\u002F vite.config.ts\n",[58,824,825,828],{"class":60,"line":68},[58,826,827],{"class":75},"build",[58,829,830],{"class":92},": {\n",[58,832,833,836],{"class":60,"line":79},[58,834,835],{"class":75},"  rollupOptions",[58,837,830],{"class":92},[58,839,840,843],{"class":60,"line":86},[58,841,842],{"class":75},"    output",[58,844,830],{"class":92},[58,846,847,850],{"class":60,"line":96},[58,848,849],{"class":75},"      chunkFileNames",[58,851,852],{"class":92},"(chunkInfo) {\n",[58,854,855,858,861,864,866,869,871,874,876,879,882,885,887,890,893,896],{"class":60,"line":109},[58,856,857],{"class":71},"        return",[58,859,860],{"class":99}," `assets\u002F${",[58,862,863],{"class":92},"chunkInfo",[58,865,202],{"class":99},[58,867,868],{"class":92},"name",[58,870,202],{"class":99},[58,872,873],{"class":75},"replace",[58,875,273],{"class":99},[58,877,878],{"class":99},"\u002F",[58,880,881],{"class":71},"^",[58,883,761],{"class":884},"sPKmS",[58,886,878],{"class":99},[58,888,889],{"class":99},", ",[58,891,892],{"class":99},"''",[58,894,895],{"class":99},")",[58,897,898],{"class":99},"}-[hash].js`\n",[58,900,901],{"class":60,"line":119},[58,902,903],{"class":92},"      },\n",[58,905,906],{"class":60,"line":129},[58,907,908],{"class":92},"    },\n",[58,910,911],{"class":60,"line":139},[58,912,913],{"class":92},"  },\n",[58,915,916],{"class":60,"line":149},[58,917,918],{"class":92},"},\n",[15,920,921,922,924],{},"这样就避免产生 ",[19,923,761],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[44,926,928],{"id":927},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[15,930,931,932,935],{},"多次 ",[19,933,934],{},"go build"," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[15,937,938,940,941,944],{},[737,939,739],{},"：使用 ",[19,942,943],{},"go build -a"," 强制重新编译所有包。",[11,946,947],{"id":947},"最终效果",[49,949,951],{"className":579,"code":950,"language":581,"meta":54,"style":54},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[19,952,953,964,975],{"__ignoreMap":54},[58,954,955,958,961],{"class":60,"line":61},[58,956,957],{"class":75},"$",[58,959,960],{"class":99}," bash",[58,962,963],{"class":99}," build-standalone.sh\n",[58,965,966,969,972],{"class":60,"line":68},[58,967,968],{"class":99},"===",[58,970,971],{"class":99}," 构建成功",[58,973,974],{"class":99}," ===\n",[58,976,977,980,983],{"class":60,"line":79},[58,978,979],{"class":75},"输出:",[58,981,982],{"class":99}," service\u002Fmyapp.exe",[58,984,985],{"class":99},"  41M\n",[15,987,988],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[15,990,991],{},[737,992,993],{},"部署对比：",[995,996,997,1013],"table",{},[998,999,1000],"thead",{},[1001,1002,1003,1007,1010],"tr",{},[1004,1005,1006],"th",{},"方式",[1004,1008,1009],{},"文件数",[1004,1011,1012],{},"操作",[1014,1015,1016,1028],"tbody",{},[1001,1017,1018,1022,1025],{},[1019,1020,1021],"td",{},"旧方案",[1019,1023,1024],{},"1 exe + 1 web 目录",[1019,1026,1027],{},"解压→保持目录结构",[1001,1029,1030,1033,1036],{},[1019,1031,1032],{},"新方案",[1019,1034,1035],{},"1 个 exe",[1019,1037,1038],{},"直接扔到服务器运行",[11,1040,1041],{"id":1041},"总结",[15,1043,1044,1045,1047],{},"Go 的 ",[19,1046,34],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[1049,1050,1051,1059],"ol",{},[1052,1053,1054,1055,778,1057,781],"li",{},"嵌入目录时自动排除 ",[19,1056,761],{},[19,1058,202],{},[1052,1060,1061],{},"构建缓存可能导致 embed 不是最新的",[15,1063,1064],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[1066,1067,1068],"style",{},"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":54,"searchDepth":68,"depth":79,"links":1070},[1071,1072,1073,1078,1084,1085],{"id":13,"depth":68,"text":13},{"id":28,"depth":68,"text":28},{"id":42,"depth":68,"text":42,"children":1074},[1075,1076,1077],{"id":46,"depth":79,"text":47},{"id":460,"depth":79,"text":461},{"id":575,"depth":79,"text":576},{"id":720,"depth":68,"text":720,"children":1079},[1080,1081,1083],{"id":723,"depth":79,"text":724},{"id":754,"depth":79,"text":1082},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":927,"depth":79,"text":928},{"id":947,"depth":68,"text":947},{"id":1041,"depth":68,"text":1041},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","进阶",false,"md","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fgo-embed-frontend","2026-06-06T16:00:00",{"title":5,"description":1086},"Go 工程实践","posts\u002Fgo-embed-frontend\u002Findex",[1098,1099,1100,1101],"Go","Vue","编译","埋坑","2026-06-06","SIQNL24wVAFb18wIGWSB3lNVZUQ9B76jWgQ55TASTYk",[1105,1899,2309,2532,2948,4122,4407,6450,7232,8014],{"id":1106,"title":1107,"author":6,"body":1108,"description":1887,"difficulty":1087,"draft":1088,"extension":1089,"image":1888,"meta":1889,"navigation":82,"path":1890,"pinned":1088,"published":1891,"seo":1892,"series":1893,"seriesOrder":61,"stem":1894,"tags":1895,"updated":1891,"__hash__":1898},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":8,"value":1109,"toc":1876},[1110,1113,1116,1119,1336,1339,1381,1384,1402,1405,1553,1556,1692,1695,1698,1756,1759,1873],[11,1111,1112],{"id":1112},"解决问题",[15,1114,1115],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[11,1117,1118],{"id":1118},"查阅资料",[49,1120,1122],{"className":579,"code":1121,"language":581,"meta":54,"style":54},"# 启动容器并挂载当前目录到 `\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",[19,1123,1124,1129,1166,1170,1175,1202,1218,1233,1238,1250,1270,1278,1282,1287,1297,1307,1311,1319,1323],{"__ignoreMap":54},[58,1125,1126],{"class":60,"line":61},[58,1127,1128],{"class":64},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[58,1130,1131,1134,1137,1140,1143,1146,1148,1151,1154,1157,1160,1163],{"class":60,"line":68},[58,1132,1133],{"class":75},"docker",[58,1135,1136],{"class":99}," run",[58,1138,1139],{"class":382}," -it",[58,1141,1142],{"class":382}," --rm",[58,1144,1145],{"class":382}," -v",[58,1147,478],{"class":99},[58,1149,1150],{"class":92},"$PWD",[58,1152,1153],{"class":99},"\":\u002Fapp",[58,1155,1156],{"class":382}," -w",[58,1158,1159],{"class":99}," \u002Fapp",[58,1161,1162],{"class":99}," ubuntu:20.04",[58,1164,1165],{"class":99}," bash\n",[58,1167,1168],{"class":60,"line":79},[58,1169,83],{"emptyLinePlaceholder":82},[58,1171,1172],{"class":60,"line":86},[58,1173,1174],{"class":64},"# 在容器内执行以下操作：\n",[58,1176,1177,1180,1183,1186,1188,1190,1193,1196,1199],{"class":60,"line":96},[58,1178,1179],{"class":75},"apt-get",[58,1181,1182],{"class":99}," update",[58,1184,1185],{"class":92}," && ",[58,1187,1179],{"class":75},[58,1189,622],{"class":99},[58,1191,1192],{"class":382}," -y",[58,1194,1195],{"class":99}," musl-tools",[58,1197,1198],{"class":99}," golang-go",[58,1200,1201],{"class":64},"  # 安装 musl 和 Go\n",[58,1203,1204,1207,1210,1212,1215],{"class":60,"line":109},[58,1205,1206],{"class":71},"export",[58,1208,1209],{"class":92}," CC",[58,1211,338],{"class":71},[58,1213,1214],{"class":92},"musl-gcc  ",[58,1216,1217],{"class":64},"# 指定 musl 编译器\n",[58,1219,1220,1222,1225,1227,1230],{"class":60,"line":119},[58,1221,1206],{"class":71},[58,1223,1224],{"class":92}," CGO_ENABLED",[58,1226,338],{"class":71},[58,1228,1229],{"class":382},"1",[58,1231,1232],{"class":64},"  # 启用 CGO\n",[58,1234,1235],{"class":60,"line":129},[58,1236,1237],{"class":64},"# 设置代理\n",[58,1239,1240,1242,1245,1247],{"class":60,"line":139},[58,1241,53],{"class":75},[58,1243,1244],{"class":99}," env",[58,1246,1156],{"class":382},[58,1248,1249],{"class":99}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[58,1251,1252,1254,1256,1259,1262,1264,1267],{"class":60,"line":149},[58,1253,53],{"class":75},[58,1255,693],{"class":99},[58,1257,1258],{"class":382}," -ldflags",[58,1260,1261],{"class":99}," '-linkmode external -extldflags \"-static\"'",[58,1263,696],{"class":382},[58,1265,1266],{"class":99}," myapp",[58,1268,1269],{"class":64},"  # 静态编译\n",[58,1271,1272,1275],{"class":60,"line":159},[58,1273,1274],{"class":382},"exit",[58,1276,1277],{"class":64},"  # 退出容器\n",[58,1279,1280],{"class":60,"line":164},[58,1281,83],{"emptyLinePlaceholder":82},[58,1283,1284],{"class":60,"line":174},[58,1285,1286],{"class":64},"# 验证结果（在宿主机执行）\n",[58,1288,1289,1292,1294],{"class":60,"line":180},[58,1290,1291],{"class":75},"file",[58,1293,1266],{"class":99},[58,1295,1296],{"class":64},"  # 应显示 \"statically linked\"\n",[58,1298,1299,1302,1304],{"class":60,"line":185},[58,1300,1301],{"class":75},"ldd",[58,1303,1266],{"class":99},[58,1305,1306],{"class":64},"   # 应显示 \"not a dynamic executable\"\n",[58,1308,1309],{"class":60,"line":191},[58,1310,83],{"emptyLinePlaceholder":82},[58,1312,1313,1316],{"class":60,"line":208},[58,1314,1315],{"class":75},"wget",[58,1317,1318],{"class":99}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[58,1320,1321],{"class":60,"line":213},[58,1322,83],{"emptyLinePlaceholder":82},[58,1324,1325,1328,1331,1334],{"class":60,"line":236},[58,1326,1327],{"class":75},"ENV",[58,1329,1330],{"class":99}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[58,1332,1333],{"class":92},"$PATH",[58,1335,106],{"class":99},[44,1337,1338],{"id":1338},"遇到问题",[1049,1340,1341,1367],{},[1052,1342,1343,1346,1347,1350,1353,1354,1357,1359,1360,1362],{},[19,1344,1345],{},"go mod tidy"," 时报错 ",[19,1348,1349],{},"tls: failed to verify certificate: x509: certificate",[1351,1352],"br",{},"解决：",[19,1355,1356],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[1351,1358],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[1351,1361],{},[1363,1364],"img",{"alt":1365,"src":1366},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[1052,1368,1369,1370,1373,1374,1376,1377,1380],{},"直接安装 ",[19,1371,1372],{},"golang-go"," 其版本过低",[1351,1375],{},"解决: ",[19,1378,1379],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[11,1382,1383],{"id":1383},"最终方案",[1049,1385,1386,1393,1396],{},[1052,1387,1388,1389,1392],{},"下载 ",[19,1390,1391],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[1052,1394,1395],{},"编写 Dockerfile",[1052,1397,1398,1399],{},"执行 ",[19,1400,1401],{},"docker build -t go-build:v1 .",[15,1403,1404],{},"Dockerfile 内容：",[49,1406,1410],{"className":1407,"code":1408,"language":1409,"meta":54,"style":54},"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",[19,1411,1412,1420,1424,1435,1439,1447,1451,1461,1465,1473,1477,1482,1489,1493,1500,1505,1512,1516,1524,1528,1535,1539],{"__ignoreMap":54},[58,1413,1414,1417],{"class":60,"line":61},[58,1415,1416],{"class":71},"FROM",[58,1418,1419],{"class":92}," ubuntu:20.04\n",[58,1421,1422],{"class":60,"line":68},[58,1423,83],{"emptyLinePlaceholder":82},[58,1425,1426,1429,1432],{"class":60,"line":79},[58,1427,1428],{"class":71},"LABEL",[58,1430,1431],{"class":92}," org.opencontainers.image.authors=",[58,1433,1434],{"class":99},"\"sutong\"\n",[58,1436,1437],{"class":60,"line":86},[58,1438,83],{"emptyLinePlaceholder":82},[58,1440,1441,1444],{"class":60,"line":96},[58,1442,1443],{"class":71},"COPY",[58,1445,1446],{"class":92}," go\u002F \u002Fgo\u002F\n",[58,1448,1449],{"class":60,"line":109},[58,1450,83],{"emptyLinePlaceholder":82},[58,1452,1453,1455,1458],{"class":60,"line":119},[58,1454,1327],{"class":71},[58,1456,1457],{"class":92}," PATH=",[58,1459,1460],{"class":99},"\"\u002Fgo\u002Fbin:$PATH\"\n",[58,1462,1463],{"class":60,"line":129},[58,1464,83],{"emptyLinePlaceholder":82},[58,1466,1467,1470],{"class":60,"line":139},[58,1468,1469],{"class":71},"RUN",[58,1471,1472],{"class":92}," apt-get update && apt-get install -y musl-tools\n",[58,1474,1475],{"class":60,"line":149},[58,1476,83],{"emptyLinePlaceholder":82},[58,1478,1479],{"class":60,"line":159},[58,1480,1481],{"class":64},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[58,1483,1484,1486],{"class":60,"line":164},[58,1485,1469],{"class":71},[58,1487,1488],{"class":92}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[58,1490,1491],{"class":60,"line":174},[58,1492,1217],{"class":64},[58,1494,1495,1497],{"class":60,"line":180},[58,1496,1327],{"class":71},[58,1498,1499],{"class":92}," CC=musl-gcc\n",[58,1501,1502],{"class":60,"line":185},[58,1503,1504],{"class":64},"# 启用 CGO\n",[58,1506,1507,1509],{"class":60,"line":191},[58,1508,1327],{"class":71},[58,1510,1511],{"class":92}," CGO_ENABLED=1\n",[58,1513,1514],{"class":60,"line":208},[58,1515,83],{"emptyLinePlaceholder":82},[58,1517,1518,1521],{"class":60,"line":213},[58,1519,1520],{"class":71},"VOLUME",[58,1522,1523],{"class":92}," \u002Fdata\n",[58,1525,1526],{"class":60,"line":236},[58,1527,83],{"emptyLinePlaceholder":82},[58,1529,1530,1533],{"class":60,"line":259},[58,1531,1532],{"class":71},"WORKDIR",[58,1534,1523],{"class":92},[58,1536,1537],{"class":60,"line":264},[58,1538,83],{"emptyLinePlaceholder":82},[58,1540,1541,1544,1547,1550],{"class":60,"line":293},[58,1542,1543],{"class":71},"CMD",[58,1545,1546],{"class":92}," [",[58,1548,1549],{"class":99},"\"\u002Fbin\u002Fbash\"",[58,1551,1552],{"class":92},"]\n",[44,1554,1555],{"id":1555},"构建",[49,1557,1559],{"className":579,"code":1558,"language":581,"meta":54,"style":54},"# 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",[19,1560,1561,1566,1570,1575,1601,1605,1610,1645,1650,1654,1687],{"__ignoreMap":54},[58,1562,1563],{"class":60,"line":61},[58,1564,1565],{"class":64},"# docker build -t go-build:v1 .\n",[58,1567,1568],{"class":60,"line":68},[58,1569,83],{"emptyLinePlaceholder":82},[58,1571,1572],{"class":60,"line":79},[58,1573,1574],{"class":64},"# 直接执行编译 编译后删除\n",[58,1576,1577,1579,1581,1583,1585,1588,1591,1594,1596,1598],{"class":60,"line":86},[58,1578,1133],{"class":75},[58,1580,1136],{"class":99},[58,1582,1142],{"class":382},[58,1584,1145],{"class":382},[58,1586,1587],{"class":99}," .:\u002Fdata",[58,1589,1590],{"class":99}," go-build:v1",[58,1592,1593],{"class":99}," go",[58,1595,693],{"class":99},[58,1597,696],{"class":382},[58,1599,1600],{"class":99}," app_musl\n",[58,1602,1603],{"class":60,"line":96},[58,1604,83],{"emptyLinePlaceholder":82},[58,1606,1607],{"class":60,"line":109},[58,1608,1609],{"class":64},"# 构建编译环境\n",[58,1611,1612,1614,1616,1618,1621,1623,1625,1628,1631,1634,1637,1640,1642],{"class":60,"line":119},[58,1613,1133],{"class":75},[58,1615,1136],{"class":99},[58,1617,1139],{"class":382},[58,1619,1620],{"class":382}," -d",[58,1622,1145],{"class":382},[58,1624,1587],{"class":99},[58,1626,1627],{"class":382}," --name",[58,1629,1630],{"class":71}," \u003C",[58,1632,1633],{"class":99},"容器名",[58,1635,1636],{"class":92},"称",[58,1638,1639],{"class":71},">",[58,1641,1590],{"class":99},[58,1643,1644],{"class":99}," \u002Fbin\u002Fbash\n",[58,1646,1647],{"class":60,"line":129},[58,1648,1649],{"class":64},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[58,1651,1652],{"class":60,"line":139},[58,1653,83],{"emptyLinePlaceholder":82},[58,1655,1656,1658,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1682,1684],{"class":60,"line":149},[58,1657,1133],{"class":75},[58,1659,1660],{"class":99}," exec",[58,1662,1139],{"class":382},[58,1664,1630],{"class":71},[58,1666,1633],{"class":99},[58,1668,1636],{"class":92},[58,1670,1639],{"class":71},[58,1672,1593],{"class":99},[58,1674,693],{"class":99},[58,1676,696],{"class":382},[58,1678,1630],{"class":71},[58,1680,1681],{"class":99},"myap",[58,1683,15],{"class":92},[58,1685,1686],{"class":71},">\n",[58,1688,1689],{"class":60,"line":159},[58,1690,1691],{"class":64},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[44,1693,1694],{"id":1694},"额外注意",[15,1696,1697],{},"若没科学上网需要设置代理：",[49,1699,1701],{"className":579,"code":1700,"language":581,"meta":54,"style":54},"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",[19,1702,1703,1721,1731,1747],{"__ignoreMap":54},[58,1704,1705,1707,1709,1711,1713,1715,1717,1719],{"class":60,"line":61},[58,1706,1133],{"class":75},[58,1708,1660],{"class":99},[58,1710,1139],{"class":382},[58,1712,1630],{"class":71},[58,1714,1633],{"class":99},[58,1716,1636],{"class":92},[58,1718,1639],{"class":71},[58,1720,1644],{"class":99},[58,1722,1723,1725,1727,1729],{"class":60,"line":68},[58,1724,53],{"class":75},[58,1726,1244],{"class":99},[58,1728,1156],{"class":382},[58,1730,1249],{"class":99},[58,1732,1733,1735,1737,1739,1741,1743,1745],{"class":60,"line":79},[58,1734,53],{"class":75},[58,1736,693],{"class":99},[58,1738,1258],{"class":382},[58,1740,1261],{"class":99},[58,1742,696],{"class":382},[58,1744,1266],{"class":99},[58,1746,1269],{"class":64},[58,1748,1749,1751,1753],{"class":60,"line":86},[58,1750,53],{"class":75},[58,1752,693],{"class":99},[58,1754,1755],{"class":64}," # musl 库编译\n",[44,1757,1758],{"id":1758},"运行环境示例",[49,1760,1762],{"className":1407,"code":1761,"language":1409,"meta":54,"style":54},"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",[19,1763,1764,1771,1775,1783,1787,1794,1798,1805,1812,1816,1823,1827,1834,1838,1859,1863,1868],{"__ignoreMap":54},[58,1765,1766,1768],{"class":60,"line":61},[58,1767,1416],{"class":71},[58,1769,1770],{"class":92}," alpine:latest\n",[58,1772,1773],{"class":60,"line":68},[58,1774,83],{"emptyLinePlaceholder":82},[58,1776,1777,1779,1781],{"class":60,"line":79},[58,1778,1428],{"class":71},[58,1780,1431],{"class":92},[58,1782,1434],{"class":99},[58,1784,1785],{"class":60,"line":86},[58,1786,83],{"emptyLinePlaceholder":82},[58,1788,1789,1791],{"class":60,"line":96},[58,1790,1443],{"class":71},[58,1792,1793],{"class":92}," glog_musl \u002Fapp\u002F\n",[58,1795,1796],{"class":60,"line":109},[58,1797,83],{"emptyLinePlaceholder":82},[58,1799,1800,1802],{"class":60,"line":119},[58,1801,1443],{"class":71},[58,1803,1804],{"class":92}," conf.ini \u002Fdata\u002F\n",[58,1806,1807,1809],{"class":60,"line":129},[58,1808,1443],{"class":71},[58,1810,1811],{"class":92}," ipname.ini \u002Fdata\u002F\n",[58,1813,1814],{"class":60,"line":139},[58,1815,83],{"emptyLinePlaceholder":82},[58,1817,1818,1820],{"class":60,"line":149},[58,1819,1532],{"class":71},[58,1821,1822],{"class":92}," \u002Fapp\n",[58,1824,1825],{"class":60,"line":159},[58,1826,83],{"emptyLinePlaceholder":82},[58,1828,1829,1831],{"class":60,"line":164},[58,1830,1469],{"class":71},[58,1832,1833],{"class":92}," chmod 777 \u002Fapp\u002Fglog_musl\n",[58,1835,1836],{"class":60,"line":174},[58,1837,83],{"emptyLinePlaceholder":82},[58,1839,1840,1842,1844,1847,1849,1852,1854,1857],{"class":60,"line":180},[58,1841,1543],{"class":71},[58,1843,1546],{"class":92},[58,1845,1846],{"class":99},"\".\u002Fglog_musl\"",[58,1848,889],{"class":92},[58,1850,1851],{"class":99},"\"logsweb\"",[58,1853,889],{"class":92},[58,1855,1856],{"class":99},"\"\u002Fdata\u002Fconf.ini\"",[58,1858,1552],{"class":92},[58,1860,1861],{"class":60,"line":185},[58,1862,83],{"emptyLinePlaceholder":82},[58,1864,1865],{"class":60,"line":191},[58,1866,1867],{"class":64},"# docker build -t glog:v1 .\n",[58,1869,1870],{"class":60,"line":208},[58,1871,1872],{"class":64},"# docker run -p 6801:6801 glog:v1\n",[1066,1874,1875],{},"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":54,"searchDepth":68,"depth":79,"links":1877},[1878,1879,1882],{"id":1112,"depth":68,"text":1112},{"id":1118,"depth":68,"text":1118,"children":1880},[1881],{"id":1338,"depth":79,"text":1338},{"id":1383,"depth":68,"text":1383,"children":1883},[1884,1885,1886],{"id":1555,"depth":79,"text":1555},{"id":1694,"depth":79,"text":1694},{"id":1758,"depth":79,"text":1758},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":1107,"description":1887},"Docker 实践笔记","posts\u002Fdocker-musl-build\u002Findex",[1896,1098,1897],"Docker","运维","YLtdOOc-gMVDSNjE-ntKXz-7hff-VtvMB2dB5RXNi1o",{"id":1900,"title":1901,"author":6,"body":1902,"description":2298,"difficulty":2299,"draft":1088,"extension":1089,"image":2300,"meta":2301,"navigation":82,"path":2302,"pinned":1088,"published":2303,"seo":2304,"series":1893,"seriesOrder":68,"stem":2305,"tags":2306,"updated":2303,"__hash__":2308},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":8,"value":1903,"toc":2288},[1904,1907,1910,1913,1917,1920,1937,1941,1974,1978,1985,2035,2038,2247,2249,2252,2282,2285],[11,1905,1906],{"id":1906},"前言",[15,1908,1909],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[11,1911,1912],{"id":1912},"解决方案",[44,1914,1916],{"id":1915},"_1-安装用户管理工具","1. 安装用户管理工具",[15,1918,1919],{},"若镜像非自带用户管理：",[49,1921,1923],{"className":1407,"code":1922,"language":1409,"meta":54,"style":54},"# 权限管理\nRUN apk add --no-cache shadow\n",[19,1924,1925,1930],{"__ignoreMap":54},[58,1926,1927],{"class":60,"line":61},[58,1928,1929],{"class":64},"# 权限管理\n",[58,1931,1932,1934],{"class":60,"line":68},[58,1933,1469],{"class":71},[58,1935,1936],{"class":92}," apk add --no-cache shadow\n",[44,1938,1940],{"id":1939},"_2-创建用户并修改权限","2. 创建用户并修改权限",[49,1942,1944],{"className":1407,"code":1943,"language":1409,"meta":54,"style":54},"# 创建用户组和用户\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",[19,1945,1946,1951,1958,1962,1967],{"__ignoreMap":54},[58,1947,1948],{"class":60,"line":61},[58,1949,1950],{"class":64},"# 创建用户组和用户\n",[58,1952,1953,1955],{"class":60,"line":68},[58,1954,1469],{"class":71},[58,1956,1957],{"class":92}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[58,1959,1960],{"class":60,"line":79},[58,1961,83],{"emptyLinePlaceholder":82},[58,1963,1964],{"class":60,"line":86},[58,1965,1966],{"class":64},"# 修改目录权限\n",[58,1968,1969,1971],{"class":60,"line":96},[58,1970,1469],{"class":71},[58,1972,1973],{"class":92}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[44,1975,1977],{"id":1976},"_3-指定用户启动","3. 指定用户启动",[15,1979,1980,1981,1984],{},"执行 docker 时加 ",[19,1982,1983],{},"--user"," 参数：",[49,1986,1988],{"className":579,"code":1987,"language":581,"meta":54,"style":54},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[19,1989,1990],{"__ignoreMap":54},[58,1991,1992,1994,1996,1998,2001,2003,2006,2009,2011,2013,2016,2019,2021,2024,2027,2030,2033],{"class":60,"line":61},[58,1993,1133],{"class":75},[58,1995,1136],{"class":99},[58,1997,1139],{"class":382},[58,1999,2000],{"class":382}," --user",[58,2002,1630],{"class":71},[58,2004,2005],{"class":99},"app_use",[58,2007,2008],{"class":92},"r",[58,2010,1639],{"class":71},[58,2012,1630],{"class":71},[58,2014,2015],{"class":99},"app_imag",[58,2017,2018],{"class":92},"e",[58,2020,1639],{"class":71},[58,2022,2023],{"class":99},":",[58,2025,2026],{"class":71},"\u003C",[58,2028,2029],{"class":99},"image_versio",[58,2031,2032],{"class":92},"n",[58,2034,1686],{"class":71},[11,2036,2037],{"id":2037},"完整示例",[49,2039,2041],{"className":1407,"code":2040,"language":1409,"meta":54,"style":54},"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",[19,2042,2043,2049,2053,2058,2071,2075,2082,2086,2092,2097,2104,2109,2116,2120,2127,2131,2135,2142,2146,2153,2157,2164,2168,2181,2187,2191,2196,2203,2210,2214,2219,2227],{"__ignoreMap":54},[58,2044,2045,2047],{"class":60,"line":61},[58,2046,1416],{"class":71},[58,2048,1770],{"class":92},[58,2050,2051],{"class":60,"line":68},[58,2052,83],{"emptyLinePlaceholder":82},[58,2054,2055],{"class":60,"line":79},[58,2056,2057],{"class":64},"# 国内源\n",[58,2059,2060,2062,2065,2068],{"class":60,"line":86},[58,2061,1469],{"class":71},[58,2063,2064],{"class":92}," sed -i ",[58,2066,2067],{"class":99},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[58,2069,2070],{"class":92}," \u002Fetc\u002Fapk\u002Frepositories\n",[58,2072,2073],{"class":60,"line":96},[58,2074,83],{"emptyLinePlaceholder":82},[58,2076,2077,2079],{"class":60,"line":109},[58,2078,1469],{"class":71},[58,2080,2081],{"class":92}," apk update --no-cache\n",[58,2083,2084],{"class":60,"line":119},[58,2085,1929],{"class":64},[58,2087,2088,2090],{"class":60,"line":129},[58,2089,1469],{"class":71},[58,2091,1936],{"class":92},[58,2093,2094],{"class":60,"line":139},[58,2095,2096],{"class":64},"# 调试\n",[58,2098,2099,2101],{"class":60,"line":149},[58,2100,1469],{"class":71},[58,2102,2103],{"class":92}," apk add --no-cache bash\n",[58,2105,2106],{"class":60,"line":159},[58,2107,2108],{"class":64},"# 设置时区\n",[58,2110,2111,2113],{"class":60,"line":164},[58,2112,1469],{"class":71},[58,2114,2115],{"class":92}," apk add --no-cache tzdata\n",[58,2117,2118],{"class":60,"line":174},[58,2119,83],{"emptyLinePlaceholder":82},[58,2121,2122,2124],{"class":60,"line":180},[58,2123,1327],{"class":71},[58,2125,2126],{"class":92}," TZ=Asia\u002FShanghai\n",[58,2128,2129],{"class":60,"line":185},[58,2130,83],{"emptyLinePlaceholder":82},[58,2132,2133],{"class":60,"line":191},[58,2134,1950],{"class":64},[58,2136,2137,2139],{"class":60,"line":208},[58,2138,1469],{"class":71},[58,2140,2141],{"class":92}," addgroup -S glog && adduser -S -G glog glog\n",[58,2143,2144],{"class":60,"line":213},[58,2145,83],{"emptyLinePlaceholder":82},[58,2147,2148,2150],{"class":60,"line":236},[58,2149,1443],{"class":71},[58,2151,2152],{"class":92}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[58,2154,2155],{"class":60,"line":259},[58,2156,83],{"emptyLinePlaceholder":82},[58,2158,2159,2161],{"class":60,"line":264},[58,2160,1469],{"class":71},[58,2162,2163],{"class":92}," mkdir \u002Fdata\n",[58,2165,2166],{"class":60,"line":293},[58,2167,83],{"emptyLinePlaceholder":82},[58,2169,2170,2172,2175,2178],{"class":60,"line":315},[58,2171,1520],{"class":71},[58,2173,2174],{"class":92}," [ ",[58,2176,2177],{"class":99},"\"\u002Fdata\"",[58,2179,2180],{"class":92}," ]\n",[58,2182,2183,2185],{"class":60,"line":332},[58,2184,1532],{"class":71},[58,2186,1523],{"class":92},[58,2188,2189],{"class":60,"line":344},[58,2190,83],{"emptyLinePlaceholder":82},[58,2192,2193],{"class":60,"line":350},[58,2194,2195],{"class":64},"# 修改权限\n",[58,2197,2198,2200],{"class":60,"line":355},[58,2199,1469],{"class":71},[58,2201,2202],{"class":92}," chown -R glog:glog \u002Fdata\n",[58,2204,2205,2207],{"class":60,"line":371},[58,2206,1469],{"class":71},[58,2208,2209],{"class":92}," chown -R glog:glog \u002Fapp\n",[58,2211,2212],{"class":60,"line":388},[58,2213,83],{"emptyLinePlaceholder":82},[58,2215,2216],{"class":60,"line":394},[58,2217,2218],{"class":64},"# 指定用户启动\n",[58,2220,2221,2224],{"class":60,"line":414},[58,2222,2223],{"class":71},"USER",[58,2225,2226],{"class":92}," glog\n",[58,2228,2229,2231,2233,2236,2238,2240,2242,2245],{"class":60,"line":419},[58,2230,1543],{"class":71},[58,2232,2174],{"class":92},[58,2234,2235],{"class":99},"\"\u002Fapp\u002Fglog_static_musl_1\"",[58,2237,889],{"class":92},[58,2239,1851],{"class":99},[58,2241,889],{"class":92},[58,2243,2244],{"class":99},"\".\u002Fconf.ini\"",[58,2246,2180],{"class":92},[11,2248,1041],{"id":1041},[15,2250,2251],{},"关键步骤：",[1049,2253,2254,2260,2270,2276],{},[1052,2255,2256,2259],{},[19,2257,2258],{},"apk add --no-cache shadow"," - 安装用户管理工具",[1052,2261,2262,2265,2266,2269],{},[19,2263,2264],{},"addgroup"," + ",[19,2267,2268],{},"adduser"," - 创建用户",[1052,2271,2272,2275],{},[19,2273,2274],{},"chown -R"," - 修改目录权限",[1052,2277,2278,2281],{},[19,2279,2280],{},"USER \u003Cusername>"," - 指定运行用户",[15,2283,2284],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[1066,2286,2287],{},"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":54,"searchDepth":68,"depth":79,"links":2289},[2290,2291,2296,2297],{"id":1906,"depth":68,"text":1906},{"id":1912,"depth":68,"text":1912,"children":2292},[2293,2294,2295],{"id":1915,"depth":79,"text":1916},{"id":1939,"depth":79,"text":1940},{"id":1976,"depth":79,"text":1977},{"id":2037,"depth":68,"text":2037},{"id":1041,"depth":68,"text":1041},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","入门","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":1901,"description":2298},"posts\u002Fdocker-user-start\u002Findex",[1896,1897,2307],"Linux","b4Uyw5ms87xr7NhpIkavHnNbT20oaEPtXz8k1w6UPj0",{"id":2310,"title":2311,"author":6,"body":2312,"description":2522,"difficulty":6,"draft":1088,"extension":1089,"image":2523,"meta":2524,"navigation":82,"path":2525,"pinned":1088,"published":2526,"seo":2527,"series":6,"seriesOrder":6,"stem":2528,"tags":2529,"updated":6,"__hash__":2531},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":8,"value":2313,"toc":2512},[2314,2318,2321,2327,2330,2334,2341,2392,2396,2402,2410,2414,2417,2420,2490,2492,2503,2509],[11,2315,2317],{"id":2316},"什么是-sveltekit","什么是 SvelteKit？",[15,2319,2320],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[15,2322,2323],{},[1363,2324],{"alt":2325,"src":2326},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[11,2328,2329],{"id":2329},"核心特点",[44,2331,2333],{"id":2332},"_1-编译时框架","1. 编译时框架",[15,2335,2336,2337,2340],{},"Svelte 和 React\u002FVue 最大的区别在于：",[737,2338,2339],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[49,2342,2346],{"className":2343,"code":2344,"language":2345,"meta":54,"style":54},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n","typescript",[19,2347,2348,2353,2366,2377,2388],{"__ignoreMap":54},[58,2349,2350],{"class":60,"line":61},[58,2351,2352],{"class":64},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[58,2354,2355,2358,2361,2363],{"class":60,"line":68},[58,2356,2357],{"class":71},"let",[58,2359,2360],{"class":92}," count ",[58,2362,338],{"class":71},[58,2364,2365],{"class":382}," 0\n",[58,2367,2368,2371,2374],{"class":60,"line":79},[58,2369,2370],{"class":71},"function",[58,2372,2373],{"class":75}," increment",[58,2375,2376],{"class":92},"() {\n",[58,2378,2379,2382,2385],{"class":60,"line":86},[58,2380,2381],{"class":92},"  count ",[58,2383,2384],{"class":71},"+=",[58,2386,2387],{"class":382}," 1\n",[58,2389,2390],{"class":60,"line":96},[58,2391,457],{"class":92},[44,2393,2395],{"id":2394},"_2-文件路由","2. 文件路由",[15,2397,2398,2401],{},[19,2399,2400],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[49,2403,2408],{"className":2404,"code":2406,"language":2407},[2405],"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",[19,2409,2406],{"__ignoreMap":54},[44,2411,2413],{"id":2412},"_3-全栈能力","3. 全栈能力",[15,2415,2416],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[11,2418,2419],{"id":2419},"和其他框架对比",[995,2421,2422,2438],{},[998,2423,2424],{},[1001,2425,2426,2429,2432,2435],{},[1004,2427,2428],{},"特性",[1004,2430,2431],{},"SvelteKit",[1004,2433,2434],{},"Next.js",[1004,2436,2437],{},"Nuxt",[1014,2439,2440,2453,2466,2479],{},[1001,2441,2442,2445,2448,2451],{},[1019,2443,2444],{},"学习曲线",[1019,2446,2447],{},"低",[1019,2449,2450],{},"中",[1019,2452,2450],{},[1001,2454,2455,2458,2461,2464],{},[1019,2456,2457],{},"包体积",[1019,2459,2460],{},"小",[1019,2462,2463],{},"大",[1019,2465,2450],{},[1001,2467,2468,2471,2474,2477],{},[1019,2469,2470],{},"运行时开销",[1019,2472,2473],{},"无",[1019,2475,2476],{},"有",[1019,2478,2476],{},[1001,2480,2481,2484,2486,2488],{},[1019,2482,2483],{},"生态",[1019,2485,2460],{},[1019,2487,2463],{},[1019,2489,2463],{},[11,2491,1041],{"id":1041},[15,2493,2494,2495,2498,2499,2502],{},"如果你追求",[737,2496,2497],{},"简洁的代码","和",[737,2500,2501],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[15,2504,2505],{},[1363,2506],{"alt":2507,"src":2508},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[1066,2510,2511],{},"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":54,"searchDepth":68,"depth":79,"links":2513},[2514,2515,2520,2521],{"id":2316,"depth":68,"text":2317},{"id":2329,"depth":68,"text":2329,"children":2516},[2517,2518,2519],{"id":2332,"depth":79,"text":2333},{"id":2394,"depth":79,"text":2395},{"id":2412,"depth":79,"text":2413},{"id":2419,"depth":68,"text":2419},{"id":1041,"depth":68,"text":1041},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":2311,"description":2522},"posts\u002Fsveltekit-intro\u002Findex",[2431,2530],"前端框架","vsEGyunpCWR7DiJAdyV1SPDfa57xXLITBI-lKM8UeGQ",{"id":2533,"title":2534,"author":6,"body":2535,"description":2936,"difficulty":2299,"draft":1088,"extension":1089,"image":2937,"meta":2938,"navigation":82,"path":2939,"pinned":82,"published":2940,"seo":2941,"series":2942,"seriesOrder":61,"stem":2943,"tags":2944,"updated":2946,"__hash__":2947},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎来到尼克的小窝",{"type":8,"value":2536,"toc":2927},[2537,2541,2548,2551,2554,2557,2631,2634,2637,2700,2703,2706,2771,2774,2840,2843,2846,2861,2868,2871,2902,2905,2916,2919,2924],[11,2538,2540],{"id":2539},"博客上线啦","博客上线啦！",[15,2542,2543,2544,2547],{},"Hello World！欢迎来到",[737,2545,2546],{},"尼克的小窝","，这是我的第一篇博客文章。",[15,2549,2550],{},"经过一段时间的折腾，这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。",[11,2552,2553],{"id":2553},"技术栈",[15,2555,2556],{},"本站使用了以下技术：",[995,2558,2559,2569],{},[998,2560,2561],{},[1001,2562,2563,2566],{},[1004,2564,2565],{},"技术",[1004,2567,2568],{},"用途",[1014,2570,2571,2581,2591,2601,2611,2621],{},[1001,2572,2573,2578],{},[1019,2574,2575],{},[737,2576,2577],{},"Nuxt 3",[1019,2579,2580],{},"Vue 3 全栈框架，提供 SSG、路由、API 等能力",[1001,2582,2583,2588],{},[1019,2584,2585],{},[737,2586,2587],{},"TypeScript",[1019,2589,2590],{},"类型安全，提升开发体验",[1001,2592,2593,2598],{},[1019,2594,2595],{},[737,2596,2597],{},"Tailwind CSS v4",[1019,2599,2600],{},"原子化 CSS，快速构建 UI",[1001,2602,2603,2608],{},[1019,2604,2605],{},[737,2606,2607],{},"@nuxt\u002Fcontent",[1019,2609,2610],{},"Markdown 驱动的内容管理",[1001,2612,2613,2618],{},[1019,2614,2615],{},[737,2616,2617],{},"Shiki",[1019,2619,2620],{},"代码语法高亮",[1001,2622,2623,2628],{},[1019,2624,2625],{},[737,2626,2627],{},"Giscus",[1019,2629,2630],{},"基于 GitHub Discussions 的评论系统",[11,2632,2633],{"id":2633},"功能特性",[15,2635,2636],{},"目前已实现的功能：",[2638,2639,2640,2646,2652,2658,2664,2670,2676,2682,2688,2694],"ul",{},[1052,2641,2642,2645],{},[737,2643,2644],{},"博客系统"," - Markdown 文章，支持代码高亮、目录导航、搜索",[1052,2647,2648,2651],{},[737,2649,2650],{},"暗色模式"," - 跟随系统或手动切换",[1052,2653,2654,2657],{},[737,2655,2656],{},"文章搜索"," - 支持标题、描述、正文、标签多维度搜索",[1052,2659,2660,2663],{},[737,2661,2662],{},"标签系统"," - 文章标签分类，高频标签快速筛选",[1052,2665,2666,2669],{},[737,2667,2668],{},"友链页面"," - 展示友情链接，支持申请",[1052,2671,2672,2675],{},[737,2673,2674],{},"赞助页面"," - 微信收款码展示",[1052,2677,2678,2681],{},[737,2679,2680],{},"封面制作"," - 在线生成封面图片工具",[1052,2683,2684,2687],{},[737,2685,2686],{},"网易云热评"," - 首页随机展示音乐评论",[1052,2689,2690,2693],{},[737,2691,2692],{},"访客统计"," - 不蒜子计数器，带数字滚动动画",[1052,2695,2696,2699],{},[737,2697,2698],{},"SEO 优化"," - 自动生成站点地图、Open Graph 标签",[11,2701,2702],{"id":2702},"代码示例",[15,2704,2705],{},"本站的配置非常集中，一个文件搞定全站定制：",[49,2707,2709],{"className":2343,"code":2708,"language":2345,"meta":54,"style":54},"\u002F\u002F config\u002Fsite.ts\nexport const siteConfig = {\n  siteName: '尼克的小窝',\n  url: 'https:\u002F\u002Fnixus.top\u002F',\n  avatar: 'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100',\n  \u002F\u002F ... 改这一个文件，全站生效\n}\n",[19,2710,2711,2716,2731,2742,2752,2762,2767],{"__ignoreMap":54},[58,2712,2713],{"class":60,"line":61},[58,2714,2715],{"class":64},"\u002F\u002F config\u002Fsite.ts\n",[58,2717,2718,2720,2723,2726,2729],{"class":60,"line":68},[58,2719,1206],{"class":71},[58,2721,2722],{"class":71}," const",[58,2724,2725],{"class":382}," siteConfig",[58,2727,2728],{"class":71}," =",[58,2730,233],{"class":92},[58,2732,2733,2736,2739],{"class":60,"line":79},[58,2734,2735],{"class":92},"  siteName: ",[58,2737,2738],{"class":99},"'尼克的小窝'",[58,2740,2741],{"class":92},",\n",[58,2743,2744,2747,2750],{"class":60,"line":86},[58,2745,2746],{"class":92},"  url: ",[58,2748,2749],{"class":99},"'https:\u002F\u002Fnixus.top\u002F'",[58,2751,2741],{"class":92},[58,2753,2754,2757,2760],{"class":60,"line":96},[58,2755,2756],{"class":92},"  avatar: ",[58,2758,2759],{"class":99},"'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100'",[58,2761,2741],{"class":92},[58,2763,2764],{"class":60,"line":109},[58,2765,2766],{"class":64},"  \u002F\u002F ... 改这一个文件，全站生效\n",[58,2768,2769],{"class":60,"line":119},[58,2770,457],{"class":92},[15,2772,2773],{},"文章的 frontmatter 也很简洁：",[49,2775,2779],{"className":2776,"code":2777,"language":2778,"meta":54,"style":54},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 文章标题\npublished: 2026-05-31T08:00:00\ndescription: 文章简介\ntags: ['Nuxt', 'TypeScript']\n---\n","yaml",[19,2780,2781,2786,2798,2808,2818,2836],{"__ignoreMap":54},[58,2782,2783],{"class":60,"line":61},[58,2784,2785],{"class":75},"---\n",[58,2787,2788,2792,2795],{"class":60,"line":68},[58,2789,2791],{"class":2790},"sbB4o","title",[58,2793,2794],{"class":92},": ",[58,2796,2797],{"class":99},"文章标题\n",[58,2799,2800,2803,2805],{"class":60,"line":79},[58,2801,2802],{"class":2790},"published",[58,2804,2794],{"class":92},[58,2806,2807],{"class":382},"2026-05-31T08:00:00\n",[58,2809,2810,2813,2815],{"class":60,"line":86},[58,2811,2812],{"class":2790},"description",[58,2814,2794],{"class":92},[58,2816,2817],{"class":99},"文章简介\n",[58,2819,2820,2823,2826,2829,2831,2834],{"class":60,"line":96},[58,2821,2822],{"class":2790},"tags",[58,2824,2825],{"class":92},": [",[58,2827,2828],{"class":99},"'Nuxt'",[58,2830,889],{"class":92},[58,2832,2833],{"class":99},"'TypeScript'",[58,2835,1552],{"class":92},[58,2837,2838],{"class":60,"line":109},[58,2839,2785],{"class":75},[11,2841,2842],{"id":2842},"部署",[15,2844,2845],{},"本站使用 SSG 静态生成，构建后直接部署到静态托管即可：",[49,2847,2849],{"className":579,"code":2848,"language":581,"meta":54,"style":54},"npx nuxi generate\n",[19,2850,2851],{"__ignoreMap":54},[58,2852,2853,2855,2858],{"class":60,"line":61},[58,2854,630],{"class":75},[58,2856,2857],{"class":99}," nuxi",[58,2859,2860],{"class":99}," generate\n",[15,2862,2863,2864,2867],{},"构建产物在 ",[19,2865,2866],{},".output\u002Fpublic"," 目录下，可以直接丢到 Nginx、Vercel、Netlify 等平台。",[11,2869,2870],{"id":2870},"后续计划",[2638,2872,2875,2884,2890,2896],{"className":2873},[2874],"contains-task-list",[1052,2876,2879,2883],{"className":2877},[2878],"task-list-item",[2880,2881],"input",{"disabled":82,"type":2882},"checkbox"," RSS 订阅",[1052,2885,2887,2889],{"className":2886},[2878],[2880,2888],{"disabled":82,"type":2882}," 文章目录优化",[1052,2891,2893,2895],{"className":2892},[2878],[2880,2894],{"disabled":82,"type":2882}," 更多小工具",[1052,2897,2899,2901],{"className":2898},[2878],[2880,2900],{"disabled":82,"type":2882}," 持续输出内容",[11,2903,2904],{"id":2904},"致谢",[15,2906,2907,2908,2915],{},"感谢 ",[2909,2910,2914],"a",{"href":2911,"rel":2912},"https:\u002F\u002F2x.nz",[2913],"nofollow","二叉树树"," 的开源项目，本站的 UI 设计参考了他的项目。",[2917,2918],"hr",{},[15,2920,2921],{},[737,2922,2923],{},"感谢阅读！如果觉得不错，欢迎点个 Star 或者留个评论 (☞ﾟヮﾟ)☞",[1066,2925,2926],{},"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":54,"searchDepth":68,"depth":79,"links":2928},[2929,2930,2931,2932,2933,2934,2935],{"id":2539,"depth":68,"text":2540},{"id":2553,"depth":68,"text":2553},{"id":2633,"depth":68,"text":2633},{"id":2702,"depth":68,"text":2702},{"id":2842,"depth":68,"text":2842},{"id":2870,"depth":68,"text":2870},{"id":2904,"depth":68,"text":2904},"博客上线啦！这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。","\u002Fposts\u002Fhello-world\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fhello-world","2026-05-31T08:00:00",{"title":2534,"description":2936},"Nuxt 博客开发","posts\u002Fhello-world\u002Findex",[2945,2437],"博客","2026-05-31","s0QPHV3aEea6nZra0FNTkxjNO-kLBsrYzuTPbNDtFvI",{"id":2949,"title":2950,"author":6,"body":2951,"description":4111,"difficulty":1087,"draft":1088,"extension":1089,"image":4112,"meta":4113,"navigation":82,"path":4114,"pinned":1088,"published":4115,"seo":4116,"series":2942,"seriesOrder":68,"stem":4117,"tags":4118,"updated":2946,"__hash__":4121},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",{"type":8,"value":2952,"toc":4101},[2953,2956,2959,2962,2966,2969,2979,2982,2989,3055,3062,3064,3071,3341,3351,3355,3358,3361,3386,3400,3403,3426,3432,3435,3438,3441,3446,3453,3678,3683,3727,3731,3734,3741,3744,3794,3800,4028,4038,4040,4092,4095,4098],[2954,2955,13],"h1",{"id":13},[15,2957,2958],{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[15,2960,2961],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[2954,2963,2965],{"id":2964},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[11,2967,2968],{"id":2968},"现象",[15,2970,2971,2972,2975,2976,782],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[19,2973,2974],{},"post.body.toc"," 上。直接用它渲染目录，发现",[737,2977,2978],{},"只有 h2 标题，h3 全部丢失",[11,2980,2981],{"id":2981},"排查",[15,2983,2984,2985,2988],{},"查了官方文档，发现 ",[19,2986,2987],{},"build.markdown.toc.depth"," 可以控制深度：",[49,2990,2992],{"className":2343,"code":2991,"language":2345,"meta":54,"style":54},"\u002F\u002F nuxt.config.ts\ncontent: {\n  build: {\n    markdown: {\n      toc: { depth: 3 }  \u002F\u002F 包含 h3\n    }\n  }\n}\n",[19,2993,2994,2999,3006,3013,3020,3042,3046,3051],{"__ignoreMap":54},[58,2995,2996],{"class":60,"line":61},[58,2997,2998],{"class":64},"\u002F\u002F nuxt.config.ts\n",[58,3000,3001,3004],{"class":60,"line":68},[58,3002,3003],{"class":75},"content",[58,3005,830],{"class":92},[58,3007,3008,3011],{"class":60,"line":79},[58,3009,3010],{"class":75},"  build",[58,3012,830],{"class":92},[58,3014,3015,3018],{"class":60,"line":86},[58,3016,3017],{"class":75},"    markdown",[58,3019,830],{"class":92},[58,3021,3022,3025,3028,3031,3033,3036,3039],{"class":60,"line":96},[58,3023,3024],{"class":75},"      toc",[58,3026,3027],{"class":92},": { ",[58,3029,3030],{"class":75},"depth",[58,3032,2794],{"class":92},[58,3034,3035],{"class":382},"3",[58,3037,3038],{"class":92}," }  ",[58,3040,3041],{"class":64},"\u002F\u002F 包含 h3\n",[58,3043,3044],{"class":60,"line":109},[58,3045,451],{"class":92},[58,3047,3048],{"class":60,"line":119},[58,3049,3050],{"class":92},"  }\n",[58,3052,3053],{"class":60,"line":129},[58,3054,457],{"class":92},[15,3056,3057,3058,3061],{},"配置改了，清了缓存重启，",[737,3059,3060],{},"依然只有 h2","。这个配置似乎不起作用。",[11,3063,1383],{"id":1383},[15,3065,3066,3067,3070],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[19,3068,3069],{},"post.body"," 的 AST 里递归提取所有标题：",[49,3072,3074],{"className":2343,"code":3073,"language":2345,"meta":54,"style":54},"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",[19,3075,3076,3118,3138,3151,3156,3211,3241,3272,3297,3301,3326,3330,3337],{"__ignoreMap":54},[58,3077,3078,3080,3083,3085,3088,3090,3093,3095,3098,3100,3103,3106,3108,3111,3113,3115],{"class":60,"line":61},[58,3079,2370],{"class":71},[58,3081,3082],{"class":75}," extractHeadings",[58,3084,273],{"class":92},[58,3086,3087],{"class":276},"node",[58,3089,2023],{"class":71},[58,3091,3092],{"class":382}," any",[58,3094,889],{"class":92},[58,3096,3097],{"class":276},"list",[58,3099,2023],{"class":71},[58,3101,3102],{"class":75}," Heading",[58,3104,3105],{"class":92},"[] ",[58,3107,338],{"class":71},[58,3109,3110],{"class":92}," [])",[58,3112,2023],{"class":71},[58,3114,3102],{"class":75},[58,3116,3117],{"class":92},"[] {\n",[58,3119,3120,3123,3126,3129,3132,3135],{"class":60,"line":68},[58,3121,3122],{"class":71},"  if",[58,3124,3125],{"class":92}," (",[58,3127,3128],{"class":71},"!",[58,3130,3131],{"class":92},"node) ",[58,3133,3134],{"class":71},"return",[58,3136,3137],{"class":92}," list\n",[58,3139,3140,3142,3145,3148],{"class":60,"line":79},[58,3141,3122],{"class":71},[58,3143,3144],{"class":92}," (Array.",[58,3146,3147],{"class":75},"isArray",[58,3149,3150],{"class":92},"(node)) {\n",[58,3152,3153],{"class":60,"line":86},[58,3154,3155],{"class":64},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[58,3157,3158,3161,3163,3166,3169,3172,3175,3177,3180,3183,3186,3188,3191,3194,3196,3198,3200,3203,3206,3208],{"class":60,"line":96},[58,3159,3160],{"class":71},"    if",[58,3162,3125],{"class":92},[58,3164,3165],{"class":71},"typeof",[58,3167,3168],{"class":92}," node[",[58,3170,3171],{"class":382},"0",[58,3173,3174],{"class":92},"] ",[58,3176,968],{"class":71},[58,3178,3179],{"class":99}," 'string'",[58,3181,3182],{"class":71}," &&",[58,3184,3185],{"class":99}," \u002F",[58,3187,881],{"class":71},[58,3189,3190],{"class":884},"h",[58,3192,3193],{"class":382},"[1-6]",[58,3195,957],{"class":71},[58,3197,878],{"class":99},[58,3199,202],{"class":92},[58,3201,3202],{"class":75},"test",[58,3204,3205],{"class":92},"(node[",[58,3207,3171],{"class":382},[58,3209,3210],{"class":92},"])) {\n",[58,3212,3213,3216,3219,3221,3224,3226,3228,3231,3234,3236,3238],{"class":60,"line":109},[58,3214,3215],{"class":71},"      const",[58,3217,3218],{"class":382}," level",[58,3220,2728],{"class":71},[58,3222,3223],{"class":75}," Number",[58,3225,3205],{"class":92},[58,3227,3171],{"class":382},[58,3229,3230],{"class":92},"].",[58,3232,3233],{"class":75},"charAt",[58,3235,273],{"class":92},[58,3237,1229],{"class":382},[58,3239,3240],{"class":92},"))\n",[58,3242,3243,3245,3248,3250,3253,3256,3259,3261,3264,3267,3270],{"class":60,"line":119},[58,3244,3215],{"class":71},[58,3246,3247],{"class":382}," text",[58,3249,2728],{"class":71},[58,3251,3252],{"class":75}," extractText",[58,3254,3255],{"class":92},"(node.",[58,3257,3258],{"class":75},"slice",[58,3260,273],{"class":92},[58,3262,3263],{"class":382},"2",[58,3265,3266],{"class":92},")).",[58,3268,3269],{"class":75},"trim",[58,3271,508],{"class":92},[58,3273,3274,3277,3280,3283,3285,3288,3291,3294],{"class":60,"line":129},[58,3275,3276],{"class":92},"      list.",[58,3278,3279],{"class":75},"push",[58,3281,3282],{"class":92},"({ id: node[",[58,3284,1229],{"class":382},[58,3286,3287],{"class":92},"]?.id ",[58,3289,3290],{"class":71},"||",[58,3292,3293],{"class":75}," slugify",[58,3295,3296],{"class":92},"(text), text, level })\n",[58,3298,3299],{"class":60,"line":139},[58,3300,451],{"class":92},[58,3302,3303,3306,3308,3311,3314,3317,3320,3323],{"class":60,"line":149},[58,3304,3305],{"class":71},"    for",[58,3307,3125],{"class":92},[58,3309,3310],{"class":71},"const",[58,3312,3313],{"class":382}," child",[58,3315,3316],{"class":71}," of",[58,3318,3319],{"class":92}," node) ",[58,3321,3322],{"class":75},"extractHeadings",[58,3324,3325],{"class":92},"(child, list)\n",[58,3327,3328],{"class":60,"line":159},[58,3329,3050],{"class":92},[58,3331,3332,3335],{"class":60,"line":164},[58,3333,3334],{"class":71},"  return",[58,3336,3137],{"class":92},[58,3338,3339],{"class":60,"line":174},[58,3340,457],{"class":92},[15,3342,3343,3344,3346,3347,3350],{},"关键点：Nuxt Content v3 的 body 使用 ",[737,3345,8],{}," 格式，标题节点是数组 ",[19,3348,3349],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[2954,3352,3354],{"id":3353},"问题二图片路径解析错误","问题二：图片路径解析错误",[11,3356,2968],{"id":3357},"现象-1",[15,3359,3360],{},"markdown 里用相对路径引用图片：",[49,3362,3366],{"className":3363,"code":3364,"language":3365,"meta":54,"style":54},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[19,3367,3368],{"__ignoreMap":54},[58,3369,3370,3373,3377,3380,3384],{"class":60,"line":61},[58,3371,3372],{"class":92},"![",[58,3374,3376],{"class":3375},"smiIW","SSHFS挂载效果",[58,3378,3379],{"class":92},"](",[58,3381,3383],{"class":3382},"szbl0","img\u002Fsshfs.avif",[58,3385,177],{"class":92},[15,3387,3388,3389,3392,3393,3396,3397,782],{},"HTML 输出的 ",[19,3390,3391],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[19,3394,3395],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[19,3398,3399],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[11,3401,2981],{"id":3402},"排查-1",[1049,3404,3405,3412,3415,3421],{},[1052,3406,3407,3408,3411],{},"检查了 ",[19,3409,3410],{},"\u003Cbase>"," 标签 —— 没有",[1052,3413,3414],{},"检查了重定向 —— 没有",[1052,3416,3417,3418,3420],{},"直接访问 ",[19,3419,3399],{}," —— 返回 200 OK",[1052,3422,3417,3423,3425],{},[19,3424,3395],{}," —— 404",[15,3427,3428,3429,782],{},"服务器路由没问题，问题是",[737,3430,3431],{},"浏览器解析相对路径时基于了错误的基准 URL",[15,3433,3434],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[11,3436,1383],{"id":3437},"最终方案-1",[15,3439,3440],{},"两步解决：",[15,3442,3443],{},[737,3444,3445],{},"第一步：自定义 ProseImg 组件",[15,3447,3448,3449,3452],{},"创建 ",[19,3450,3451],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[49,3454,3458],{"className":3455,"code":3456,"language":3457,"meta":54,"style":54},"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",[19,3459,3460,3472,3486,3506,3522,3560,3593,3617,3622,3631,3635,3644,3670],{"__ignoreMap":54},[58,3461,3462,3464,3467,3470],{"class":60,"line":61},[58,3463,2026],{"class":92},[58,3465,3466],{"class":2790},"script",[58,3468,3469],{"class":75}," setup",[58,3471,1686],{"class":92},[58,3473,3474,3476,3479,3481,3484],{"class":60,"line":68},[58,3475,3310],{"class":71},[58,3477,3478],{"class":382}," route",[58,3480,2728],{"class":71},[58,3482,3483],{"class":75}," useRoute",[58,3485,508],{"class":92},[58,3487,3488,3490,3493,3495,3498,3501,3504],{"class":60,"line":79},[58,3489,3310],{"class":71},[58,3491,3492],{"class":382}," resolvedSrc",[58,3494,2728],{"class":71},[58,3496,3497],{"class":75}," computed",[58,3499,3500],{"class":92},"(() ",[58,3502,3503],{"class":71},"=>",[58,3505,233],{"class":92},[58,3507,3508,3510,3512,3514,3517,3519],{"class":60,"line":86},[58,3509,3122],{"class":71},[58,3511,3125],{"class":92},[58,3513,3128],{"class":71},[58,3515,3516],{"class":92},"props.src) ",[58,3518,3134],{"class":71},[58,3520,3521],{"class":99}," ''\n",[58,3523,3524,3526,3529,3532,3534,3537,3540,3542,3545,3547,3549,3552,3555,3557],{"class":60,"line":96},[58,3525,3122],{"class":71},[58,3527,3528],{"class":92}," (props.src.",[58,3530,3531],{"class":75},"startsWith",[58,3533,273],{"class":92},[58,3535,3536],{"class":99},"'\u002F'",[58,3538,3539],{"class":92},") ",[58,3541,3290],{"class":71},[58,3543,3544],{"class":92}," props.src.",[58,3546,3531],{"class":75},[58,3548,273],{"class":92},[58,3550,3551],{"class":99},"'http'",[58,3553,3554],{"class":92},")) ",[58,3556,3134],{"class":71},[58,3558,3559],{"class":92}," props.src\n",[58,3561,3562,3565,3568,3570,3573,3575,3577,3579,3583,3585,3587,3589,3591],{"class":60,"line":109},[58,3563,3564],{"class":71},"  const",[58,3566,3567],{"class":382}," base",[58,3569,2728],{"class":71},[58,3571,3572],{"class":92}," route.path.",[58,3574,873],{"class":75},[58,3576,273],{"class":92},[58,3578,878],{"class":99},[58,3580,3582],{"class":3581},"sCnZR","\\\u002F",[58,3584,957],{"class":71},[58,3586,878],{"class":99},[58,3588,889],{"class":92},[58,3590,892],{"class":99},[58,3592,177],{"class":92},[58,3594,3595,3597,3600,3603,3606,3609,3611,3614],{"class":60,"line":119},[58,3596,3334],{"class":71},[58,3598,3599],{"class":99}," `${",[58,3601,3602],{"class":92},"base",[58,3604,3605],{"class":99},"}\u002F${",[58,3607,3608],{"class":92},"props",[58,3610,202],{"class":99},[58,3612,3613],{"class":92},"src",[58,3615,3616],{"class":99},"}`\n",[58,3618,3619],{"class":60,"line":129},[58,3620,3621],{"class":92},"})\n",[58,3623,3624,3627,3629],{"class":60,"line":139},[58,3625,3626],{"class":92},"\u003C\u002F",[58,3628,3466],{"class":2790},[58,3630,1686],{"class":92},[58,3632,3633],{"class":60,"line":149},[58,3634,83],{"emptyLinePlaceholder":82},[58,3636,3637,3639,3642],{"class":60,"line":159},[58,3638,2026],{"class":92},[58,3640,3641],{"class":2790},"template",[58,3643,1686],{"class":92},[58,3645,3646,3649,3651,3654,3656,3659,3662,3664,3667],{"class":60,"line":164},[58,3647,3648],{"class":92},"  \u003C",[58,3650,1363],{"class":2790},[58,3652,3653],{"class":75}," :src",[58,3655,338],{"class":92},[58,3657,3658],{"class":99},"\"resolvedSrc\"",[58,3660,3661],{"class":75}," :alt",[58,3663,338],{"class":92},[58,3665,3666],{"class":99},"\"alt\"",[58,3668,3669],{"class":92}," \u002F>\n",[58,3671,3672,3674,3676],{"class":60,"line":174},[58,3673,3626],{"class":92},[58,3675,3641],{"class":2790},[58,3677,1686],{"class":92},[15,3679,3680],{},[737,3681,3682],{},"第二步：启用 MDC prose 组件",[49,3684,3686],{"className":2343,"code":3685,"language":2345,"meta":54,"style":54},"\u002F\u002F nuxt.config.ts\nmdc: {\n  components: {\n    prose: true  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n  }\n}\n",[19,3687,3688,3692,3699,3706,3719,3723],{"__ignoreMap":54},[58,3689,3690],{"class":60,"line":61},[58,3691,2998],{"class":64},[58,3693,3694,3697],{"class":60,"line":68},[58,3695,3696],{"class":75},"mdc",[58,3698,830],{"class":92},[58,3700,3701,3704],{"class":60,"line":79},[58,3702,3703],{"class":75},"  components",[58,3705,830],{"class":92},[58,3707,3708,3711,3713,3716],{"class":60,"line":86},[58,3709,3710],{"class":75},"    prose",[58,3712,2794],{"class":92},[58,3714,3715],{"class":382},"true",[58,3717,3718],{"class":64},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[58,3720,3721],{"class":60,"line":96},[58,3722,3050],{"class":92},[58,3724,3725],{"class":60,"line":109},[58,3726,457],{"class":92},[2954,3728,3730],{"id":3729},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[11,3732,2968],{"id":3733},"现象-2",[15,3735,3736,3737,3740],{},"原站（SvelteKit）的图片放在 ",[19,3738,3739],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[11,3742,3743],{"id":3743},"方案对比",[995,3745,3746,3759],{},[998,3747,3748],{},[1001,3749,3750,3753,3756],{},[1004,3751,3752],{},"方案",[1004,3754,3755],{},"优点",[1004,3757,3758],{},"缺点",[1014,3760,3761,3772,3783],{},[1001,3762,3763,3766,3769],{},[1019,3764,3765],{},"复制到 public\u002F",[1019,3767,3768],{},"简单",[1019,3770,3771],{},"文件重复，需同步",[1001,3773,3774,3777,3780],{},[1019,3775,3776],{},"Vite 插件拦截",[1019,3778,3779],{},"原站方案",[1019,3781,3782],{},"Nuxt 不直接支持",[1001,3784,3785,3788,3791],{},[1019,3786,3787],{},"Server Route",[1019,3789,3790],{},"不复制文件",[1019,3792,3793],{},"需要手动写路由",[15,3795,3796,3797,3799],{},"最终选择 ",[737,3798,3787],{},"，和原站的 Vite 插件思路一致：",[49,3801,3803],{"className":2343,"code":3802,"language":2345,"meta":54,"style":54},"\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",[19,3804,3805,3810,3836,3851,3914,3928,3932,3954,3980,3998,4017,4024],{"__ignoreMap":54},[58,3806,3807],{"class":60,"line":61},[58,3808,3809],{"class":64},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[58,3811,3812,3814,3817,3820,3822,3825,3827,3830,3832,3834],{"class":60,"line":68},[58,3813,1206],{"class":71},[58,3815,3816],{"class":71}," default",[58,3818,3819],{"class":75}," defineEventHandler",[58,3821,273],{"class":92},[58,3823,3824],{"class":71},"async",[58,3826,3125],{"class":92},[58,3828,3829],{"class":276},"event",[58,3831,3539],{"class":92},[58,3833,3503],{"class":71},[58,3835,233],{"class":92},[58,3837,3838,3840,3843,3845,3848],{"class":60,"line":79},[58,3839,3564],{"class":71},[58,3841,3842],{"class":382}," url",[58,3844,2728],{"class":71},[58,3846,3847],{"class":75}," getRequestURL",[58,3849,3850],{"class":92},"(event)\n",[58,3852,3853,3855,3858,3860,3863,3866,3868,3870,3872,3874,3877,3879,3881,3884,3886,3889,3892,3894,3896,3898,3900,3902,3904,3906,3908,3910,3912],{"class":60,"line":86},[58,3854,3564],{"class":71},[58,3856,3857],{"class":382}," match",[58,3859,2728],{"class":71},[58,3861,3862],{"class":92}," url.pathname.",[58,3864,3865],{"class":75},"match",[58,3867,273],{"class":92},[58,3869,878],{"class":99},[58,3871,881],{"class":71},[58,3873,3582],{"class":3581},[58,3875,3876],{"class":884},"posts",[58,3878,3582],{"class":3581},[58,3880,273],{"class":884},[58,3882,3883],{"class":382},"[",[58,3885,881],{"class":71},[58,3887,3888],{"class":382},"\u002F]",[58,3890,3891],{"class":71},"+",[58,3893,895],{"class":884},[58,3895,3582],{"class":3581},[58,3897,1363],{"class":884},[58,3899,3582],{"class":3581},[58,3901,273],{"class":884},[58,3903,202],{"class":382},[58,3905,3891],{"class":71},[58,3907,895],{"class":884},[58,3909,957],{"class":71},[58,3911,878],{"class":99},[58,3913,177],{"class":92},[58,3915,3916,3918,3920,3922,3925],{"class":60,"line":96},[58,3917,3122],{"class":71},[58,3919,3125],{"class":92},[58,3921,3128],{"class":71},[58,3923,3924],{"class":92},"match) ",[58,3926,3927],{"class":71},"return\n",[58,3929,3930],{"class":60,"line":109},[58,3931,83],{"emptyLinePlaceholder":82},[58,3933,3934,3936,3939,3942,3944,3947,3949,3951],{"class":60,"line":119},[58,3935,3564],{"class":71},[58,3937,3938],{"class":92}," [, ",[58,3940,3941],{"class":382},"slug",[58,3943,889],{"class":92},[58,3945,3946],{"class":382},"filename",[58,3948,3174],{"class":92},[58,3950,338],{"class":71},[58,3952,3953],{"class":92}," match\n",[58,3955,3956,3958,3961,3963,3966,3968,3971,3974,3977],{"class":60,"line":129},[58,3957,3564],{"class":71},[58,3959,3960],{"class":382}," filePath",[58,3962,2728],{"class":71},[58,3964,3965],{"class":75}," resolve",[58,3967,273],{"class":92},[58,3969,3970],{"class":99},"'content\u002Fposts'",[58,3972,3973],{"class":92},", slug, ",[58,3975,3976],{"class":99},"'img'",[58,3978,3979],{"class":92},", filename)\n",[58,3981,3982,3984,3987,3989,3992,3995],{"class":60,"line":139},[58,3983,3564],{"class":71},[58,3985,3986],{"class":382}," data",[58,3988,2728],{"class":71},[58,3990,3991],{"class":71}," await",[58,3993,3994],{"class":75}," readFile",[58,3996,3997],{"class":92},"(filePath)\n",[58,3999,4000,4003,4006,4009,4011,4014],{"class":60,"line":149},[58,4001,4002],{"class":75},"  setResponseHeader",[58,4004,4005],{"class":92},"(event, ",[58,4007,4008],{"class":99},"'content-type'",[58,4010,889],{"class":92},[58,4012,4013],{"class":382},"MIME",[58,4015,4016],{"class":92},"[ext])\n",[58,4018,4019,4021],{"class":60,"line":159},[58,4020,3334],{"class":71},[58,4022,4023],{"class":92}," data\n",[58,4025,4026],{"class":60,"line":164},[58,4027,3621],{"class":92},[15,4029,4030,4031,4033,4034,4037],{},"请求 ",[19,4032,3399],{}," → 读取 ",[19,4035,4036],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[2954,4039,1041],{"id":1041},[995,4041,4042,4054],{},[998,4043,4044],{},[1001,4045,4046,4049,4052],{},[1004,4047,4048],{},"问题",[1004,4050,4051],{},"根因",[1004,4053,3752],{},[1014,4055,4056,4070,4081],{},[1001,4057,4058,4061,4067],{},[1019,4059,4060],{},"TOC 只有 h2",[1019,4062,4063,4066],{},[19,4064,4065],{},"toc.depth"," 配置不生效",[1019,4068,4069],{},"从 body AST 手动提取",[1001,4071,4072,4075,4078],{},[1019,4073,4074],{},"图片路径错误",[1019,4076,4077],{},"SPA 路由导致相对路径解析基准错误",[1019,4079,4080],{},"ProseImg 组件 + mdc.prose 配置",[1001,4082,4083,4086,4089],{},[1019,4084,4085],{},"content 图片 404",[1019,4087,4088],{},"content 目录不暴露给浏览器",[1019,4090,4091],{},"Server Route 直接读取返回",[15,4093,4094],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[15,4096,4097],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[1066,4099,4100],{},"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":54,"searchDepth":68,"depth":79,"links":4102},[4103,4104,4105,4106,4107,4108,4109,4110],{"id":2968,"depth":68,"text":2968},{"id":2981,"depth":68,"text":2981},{"id":1383,"depth":68,"text":1383},{"id":3357,"depth":68,"text":2968},{"id":3402,"depth":68,"text":2981},{"id":3437,"depth":68,"text":1383},{"id":3733,"depth":68,"text":2968},{"id":3743,"depth":68,"text":3743},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":2950,"description":4111},"posts\u002Fnuxt-content-lessons\u002Findex",[2437,4119,4120],"踩坑","Nuxt Content","vbSt6ND1z7xt7L8FxgZ8Qe3EdxV9InsRerN3E23GC7s",{"id":4123,"title":4124,"author":4125,"body":4126,"description":4396,"difficulty":1087,"draft":1088,"extension":1089,"image":4397,"meta":4398,"navigation":82,"path":4399,"pinned":1088,"published":4400,"seo":4401,"series":2942,"seriesOrder":79,"stem":4402,"tags":4403,"updated":2946,"__hash__":4406},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题","Nixus",{"type":8,"value":4127,"toc":4389},[4128,4131,4134,4149,4152,4155,4158,4199,4210,4217,4239,4246,4250,4253,4256,4258,4261,4284,4295,4351,4356,4369,4371,4377,4386],[11,4129,4130],{"id":4130},"问题描述",[15,4132,4133],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[2638,4135,4136,4146],{},[1052,4137,4138,4139,4142,4143],{},"首次通过客户端导航（点击 ",[19,4140,4141],{},"\u003CNuxtLink>","）跳转到某个页面时，",[737,4144,4145],{},"所有链接文字都会出现下划线",[1052,4147,4148],{},"手动刷新页面后，下划线消失，样式恢复正常",[15,4150,4151],{},"这个问题在开发模式和生产构建后都会出现。",[11,4153,4154],{"id":4154},"原因分析",[15,4156,4157],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[49,4159,4163],{"className":4160,"code":4161,"language":4162,"meta":54,"style":54},"language-css shiki shiki-themes github-light github-dark","a {\n  color: inherit;\n  text-decoration: inherit;\n}\n","css",[19,4164,4165,4171,4184,4195],{"__ignoreMap":54},[58,4166,4167,4169],{"class":60,"line":61},[58,4168,2909],{"class":2790},[58,4170,233],{"class":92},[58,4172,4173,4176,4178,4181],{"class":60,"line":68},[58,4174,4175],{"class":382},"  color",[58,4177,2794],{"class":92},[58,4179,4180],{"class":382},"inherit",[58,4182,4183],{"class":92},";\n",[58,4185,4186,4189,4191,4193],{"class":60,"line":79},[58,4187,4188],{"class":382},"  text-decoration",[58,4190,2794],{"class":92},[58,4192,4180],{"class":382},[58,4194,4183],{"class":92},[58,4196,4197],{"class":60,"line":86},[58,4198,457],{"class":92},[15,4200,4201,4202,4205,4206,4209],{},"正常情况下，这会让链接继承父元素的 ",[19,4203,4204],{},"text-decoration","（通常是 ",[19,4207,4208],{},"none","），从而去掉浏览器默认的下划线。",[15,4211,4212,4213,4216],{},"问题出在 ",[737,4214,4215],{},"Nuxt 的客户端导航机制","：",[1049,4218,4219,4225,4228,4233,4236],{},[1052,4220,4221,4222,4224],{},"用户点击 ",[19,4223,4141],{},"，Vue Router 拦截导航",[1052,4226,4227],{},"新页面组件异步加载并渲染",[1052,4229,4230],{},[737,4231,4232],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[1052,4234,4235],{},"此时浏览器使用默认样式（链接带下划线）",[1052,4237,4238],{},"CSS 加载完成后，下划线消失",[15,4240,4241,4242,4245],{},"这就是典型的 ",[737,4243,4244],{},"FOUC（Flash of Unstyled Content）"," 问题。",[11,4247,4249],{"id":4248},"为什么刷新就正常了","为什么刷新就正常了？",[15,4251,4252],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[15,4254,4255],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[11,4257,1912],{"id":1912},[15,4259,4260],{},"尝试了多种方案：",[2638,4262,4263,4269,4275,4281],{},[1052,4264,4265,4268],{},[19,4266,4267],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[1052,4270,4271,4274],{},[19,4272,4273],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[1052,4276,4277,4280],{},[19,4278,4279],{},"features.inlineStyles: true"," — 无效",[1052,4282,4283],{},"页面过渡动画 — 只是掩盖，不能根治",[15,4285,4286,4287,4290,4291,4294],{},"最终有效的方案是在 ",[19,4288,4289],{},"app.vue"," 中用 ",[19,4292,4293],{},"!important"," 强制覆盖：",[49,4296,4298],{"className":3455,"code":4297,"language":3457,"meta":54,"style":54},"\u003Cstyle>\na:not(.prose a) {\n  text-decoration-line: none !important;\n}\n\u003C\u002Fstyle>\n",[19,4299,4300,4308,4325,4339,4343],{"__ignoreMap":54},[58,4301,4302,4304,4306],{"class":60,"line":61},[58,4303,2026],{"class":92},[58,4305,1066],{"class":2790},[58,4307,1686],{"class":92},[58,4309,4310,4312,4315,4317,4320,4323],{"class":60,"line":68},[58,4311,2909],{"class":2790},[58,4313,4314],{"class":75},":not",[58,4316,273],{"class":92},[58,4318,4319],{"class":75},".prose",[58,4321,4322],{"class":2790}," a",[58,4324,290],{"class":92},[58,4326,4327,4330,4332,4334,4337],{"class":60,"line":79},[58,4328,4329],{"class":382},"  text-decoration-line",[58,4331,2794],{"class":92},[58,4333,4208],{"class":382},[58,4335,4336],{"class":71}," !important",[58,4338,4183],{"class":92},[58,4340,4341],{"class":60,"line":86},[58,4342,457],{"class":92},[58,4344,4345,4347,4349],{"class":60,"line":96},[58,4346,3626],{"class":92},[58,4348,1066],{"class":2790},[58,4350,1686],{"class":92},[15,4352,4353],{},[737,4354,4355],{},"关键点：",[2638,4357,4358,4363],{},[1052,4359,4360,4362],{},[19,4361,4293],{}," 是为了对抗 Tailwind CSS layers 的优先级",[1052,4364,4365,4368],{},[19,4366,4367],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[11,4370,1041],{"id":1041},[15,4372,4373,4374,4376],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[19,4375,4293],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[15,4378,4379,4380,4385],{},"如果你有更好的方案，欢迎在 ",[2909,4381,4384],{"href":4382,"rel":4383},"https:\u002F\u002Fgithub.com\u002Fwishesl\u002FSuBlog\u002Fissues",[2913],"Issues"," 提出！",[1066,4387,4388],{},"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":54,"searchDepth":68,"depth":79,"links":4390},[4391,4392,4393,4394,4395],{"id":4130,"depth":68,"text":4130},{"id":4154,"depth":68,"text":4154},{"id":4248,"depth":68,"text":4249},{"id":1912,"depth":68,"text":1912},{"id":1041,"depth":68,"text":1041},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":4124,"description":4396},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[2437,4404,4405,4119],"Tailwind CSS","CSS","yMReEaosecX7QtrQkKoqINdTG0f9PrvEW4j27IgQF7g",{"id":4408,"title":4409,"author":6,"body":4410,"description":6436,"difficulty":1087,"draft":1088,"extension":1089,"image":6437,"meta":6438,"navigation":82,"path":6439,"pinned":1088,"published":6440,"seo":6441,"series":1095,"seriesOrder":61,"stem":6442,"tags":6443,"updated":6448,"__hash__":6449},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",{"type":8,"value":4411,"toc":6417},[4412,4416,4419,4422,4426,4433,4439,4442,4445,4465,4468,4472,4496,4500,4597,4610,4614,4775,4778,4991,4995,5006,5105,5112,5116,5385,5388,5468,5472,5475,5477,5488,5492,5498,5858,6046,6049,6105,6108,6261,6264,6267,6338,6340,6405,6414],[11,4413,4415],{"id":4414},"什么是-mcp","什么是 MCP",[15,4417,4418],{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[15,4420,4421],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[11,4423,4425],{"id":4424},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[15,4427,4428,4429,4432],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[19,4430,4431],{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[15,4434,4435,4436],{},"核心思路：",[737,4437,4438],{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[11,4440,4441],{"id":4441},"需求分析",[15,4443,4444],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[1049,4446,4447,4453,4459],{},[1052,4448,4449,4452],{},[737,4450,4451],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[1052,4454,4455,4458],{},[737,4456,4457],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[1052,4460,4461,4464],{},[737,4462,4463],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[11,4466,4467],{"id":4467},"实现步骤",[44,4469,4471],{"id":4470},"_1-安装依赖","1. 安装依赖",[49,4473,4475],{"className":579,"code":4474,"language":581,"meta":54,"style":54},"go get github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\ngo get github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[19,4476,4477,4487],{"__ignoreMap":54},[58,4478,4479,4481,4484],{"class":60,"line":61},[58,4480,53],{"class":75},[58,4482,4483],{"class":99}," get",[58,4485,4486],{"class":99}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[58,4488,4489,4491,4493],{"class":60,"line":68},[58,4490,53],{"class":75},[58,4492,4483],{"class":99},[58,4494,4495],{"class":99}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[44,4497,4499],{"id":4498},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[49,4501,4503],{"className":51,"code":4502,"language":53,"meta":54,"style":54},"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",[19,4504,4505,4516,4520,4536,4547,4558,4569,4578,4593],{"__ignoreMap":54},[58,4506,4507,4509,4511,4514],{"class":60,"line":61},[58,4508,89],{"class":71},[58,4510,478],{"class":99},[58,4512,4513],{"class":75},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[58,4515,106],{"class":99},[58,4517,4518],{"class":60,"line":68},[58,4519,83],{"emptyLinePlaceholder":82},[58,4521,4522,4525,4527,4530,4533],{"class":60,"line":79},[58,4523,4524],{"class":92},"mcpServer ",[58,4526,242],{"class":71},[58,4528,4529],{"class":92}," server.",[58,4531,4532],{"class":75},"NewMCPServer",[58,4534,4535],{"class":92},"(\n",[58,4537,4538,4541,4544],{"class":60,"line":86},[58,4539,4540],{"class":99},"    \"my-app\"",[58,4542,4543],{"class":92},",    ",[58,4545,4546],{"class":64},"\u002F\u002F 服务器名称\n",[58,4548,4549,4552,4555],{"class":60,"line":96},[58,4550,4551],{"class":99},"    \"1.0.0\"",[58,4553,4554],{"class":92},",     ",[58,4556,4557],{"class":64},"\u002F\u002F 版本\n",[58,4559,4560,4563,4566],{"class":60,"line":109},[58,4561,4562],{"class":92},"    server.",[58,4564,4565],{"class":75},"WithLogging",[58,4567,4568],{"class":92},"(),\n",[58,4570,4571,4573,4576],{"class":60,"line":119},[58,4572,4562],{"class":92},[58,4574,4575],{"class":75},"WithRecovery",[58,4577,4568],{"class":92},[58,4579,4580,4582,4585,4587,4590],{"class":60,"line":129},[58,4581,4562],{"class":92},[58,4583,4584],{"class":75},"WithInstructions",[58,4586,273],{"class":92},[58,4588,4589],{"class":99},"\"这是一个 MCP 服务器示例\"",[58,4591,4592],{"class":92},"),\n",[58,4594,4595],{"class":60,"line":139},[58,4596,177],{"class":92},[15,4598,4599,4601,4602,4605,4606,4609],{},[19,4600,4532],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[19,4603,4604],{},"tools\u002Flist","）和工具调用（",[19,4607,4608],{},"tools\u002Fcall","）的协议逻辑。",[44,4611,4613],{"id":4612},"_3-注册工具","3. 注册工具",[49,4615,4617],{"className":51,"code":4616,"language":53,"meta":54,"style":54},"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",[19,4618,4619,4630,4634,4639,4659,4674,4678,4682,4687,4746,4751,4771],{"__ignoreMap":54},[58,4620,4621,4623,4625,4628],{"class":60,"line":61},[58,4622,89],{"class":71},[58,4624,478],{"class":99},[58,4626,4627],{"class":75},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[58,4629,106],{"class":99},[58,4631,4632],{"class":60,"line":68},[58,4633,83],{"emptyLinePlaceholder":82},[58,4635,4636],{"class":60,"line":79},[58,4637,4638],{"class":64},"\u002F\u002F 定义工具\n",[58,4640,4641,4644,4646,4649,4652,4654,4657],{"class":60,"line":86},[58,4642,4643],{"class":92},"listTool ",[58,4645,242],{"class":71},[58,4647,4648],{"class":92}," mcp.",[58,4650,4651],{"class":75},"NewTool",[58,4653,273],{"class":92},[58,4655,4656],{"class":99},"\"list_items\"",[58,4658,2741],{"class":92},[58,4660,4661,4664,4667,4669,4672],{"class":60,"line":96},[58,4662,4663],{"class":92},"    mcp.",[58,4665,4666],{"class":75},"WithDescription",[58,4668,273],{"class":92},[58,4670,4671],{"class":99},"\"列出所有项目\"",[58,4673,4592],{"class":92},[58,4675,4676],{"class":60,"line":109},[58,4677,177],{"class":92},[58,4679,4680],{"class":60,"line":119},[58,4681,83],{"emptyLinePlaceholder":82},[58,4683,4684],{"class":60,"line":129},[58,4685,4686],{"class":64},"\u002F\u002F 注册处理器\n",[58,4688,4689,4692,4695,4698,4700,4702,4705,4708,4710,4712,4714,4717,4720,4722,4725,4728,4731,4734,4736,4739,4741,4744],{"class":60,"line":139},[58,4690,4691],{"class":92},"mcpServer.",[58,4693,4694],{"class":75},"AddTool",[58,4696,4697],{"class":92},"(listTool, ",[58,4699,216],{"class":71},[58,4701,273],{"class":92},[58,4703,4704],{"class":276},"ctx",[58,4706,4707],{"class":75}," context",[58,4709,202],{"class":92},[58,4711,287],{"class":75},[58,4713,889],{"class":92},[58,4715,4716],{"class":276},"req",[58,4718,4719],{"class":75}," mcp",[58,4721,202],{"class":92},[58,4723,4724],{"class":75},"CallToolRequest",[58,4726,4727],{"class":92},") (",[58,4729,4730],{"class":71},"*",[58,4732,4733],{"class":75},"mcp",[58,4735,202],{"class":92},[58,4737,4738],{"class":75},"CallToolResult",[58,4740,889],{"class":92},[58,4742,4743],{"class":71},"error",[58,4745,290],{"class":92},[58,4747,4748],{"class":60,"line":149},[58,4749,4750],{"class":64},"    \u002F\u002F 业务逻辑\n",[58,4752,4753,4755,4757,4760,4762,4765,4768],{"class":60,"line":159},[58,4754,267],{"class":71},[58,4756,4648],{"class":92},[58,4758,4759],{"class":75},"NewToolResultText",[58,4761,273],{"class":92},[58,4763,4764],{"class":99},"\"返回结果\"",[58,4766,4767],{"class":92},"), ",[58,4769,4770],{"class":382},"nil\n",[58,4772,4773],{"class":60,"line":164},[58,4774,3621],{"class":92},[15,4776,4777],{},"带参数的工具：",[49,4779,4781],{"className":51,"code":4780,"language":53,"meta":54,"style":54},"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",[19,4782,4783,4801,4814,4828,4838,4852,4857,4861,4865,4912,4931,4943,4961,4965,4970,4987],{"__ignoreMap":54},[58,4784,4785,4788,4790,4792,4794,4796,4799],{"class":60,"line":61},[58,4786,4787],{"class":92},"getTool ",[58,4789,242],{"class":71},[58,4791,4648],{"class":92},[58,4793,4651],{"class":75},[58,4795,273],{"class":92},[58,4797,4798],{"class":99},"\"get_item\"",[58,4800,2741],{"class":92},[58,4802,4803,4805,4807,4809,4812],{"class":60,"line":68},[58,4804,4663],{"class":92},[58,4806,4666],{"class":75},[58,4808,273],{"class":92},[58,4810,4811],{"class":99},"\"获取指定项目的详情\"",[58,4813,4592],{"class":92},[58,4815,4816,4818,4821,4823,4826],{"class":60,"line":79},[58,4817,4663],{"class":92},[58,4819,4820],{"class":75},"WithString",[58,4822,273],{"class":92},[58,4824,4825],{"class":99},"\"item_id\"",[58,4827,2741],{"class":92},[58,4829,4830,4833,4836],{"class":60,"line":86},[58,4831,4832],{"class":92},"        mcp.",[58,4834,4835],{"class":75},"Required",[58,4837,4568],{"class":92},[58,4839,4840,4842,4845,4847,4850],{"class":60,"line":96},[58,4841,4832],{"class":92},[58,4843,4844],{"class":75},"Description",[58,4846,273],{"class":92},[58,4848,4849],{"class":99},"\"项目 ID\"",[58,4851,4592],{"class":92},[58,4853,4854],{"class":60,"line":109},[58,4855,4856],{"class":92},"    ),\n",[58,4858,4859],{"class":60,"line":119},[58,4860,177],{"class":92},[58,4862,4863],{"class":60,"line":129},[58,4864,83],{"emptyLinePlaceholder":82},[58,4866,4867,4869,4871,4874,4876,4878,4880,4882,4884,4886,4888,4890,4892,4894,4896,4898,4900,4902,4904,4906,4908,4910],{"class":60,"line":139},[58,4868,4691],{"class":92},[58,4870,4694],{"class":75},[58,4872,4873],{"class":92},"(getTool, ",[58,4875,216],{"class":71},[58,4877,273],{"class":92},[58,4879,4704],{"class":276},[58,4881,4707],{"class":75},[58,4883,202],{"class":92},[58,4885,287],{"class":75},[58,4887,889],{"class":92},[58,4889,4716],{"class":276},[58,4891,4719],{"class":75},[58,4893,202],{"class":92},[58,4895,4724],{"class":75},[58,4897,4727],{"class":92},[58,4899,4730],{"class":71},[58,4901,4733],{"class":75},[58,4903,202],{"class":92},[58,4905,4738],{"class":75},[58,4907,889],{"class":92},[58,4909,4743],{"class":71},[58,4911,290],{"class":92},[58,4913,4914,4917,4919,4922,4925,4927,4929],{"class":60,"line":149},[58,4915,4916],{"class":92},"    itemID, err ",[58,4918,242],{"class":71},[58,4920,4921],{"class":92}," req.",[58,4923,4924],{"class":75},"RequireString",[58,4926,273],{"class":92},[58,4928,4825],{"class":99},[58,4930,177],{"class":92},[58,4932,4933,4935,4937,4939,4941],{"class":60,"line":159},[58,4934,3160],{"class":71},[58,4936,376],{"class":92},[58,4938,379],{"class":71},[58,4940,383],{"class":382},[58,4942,233],{"class":92},[58,4944,4945,4947,4949,4952,4954,4957,4959],{"class":60,"line":164},[58,4946,857],{"class":71},[58,4948,4648],{"class":92},[58,4950,4951],{"class":75},"NewToolResultError",[58,4953,273],{"class":92},[58,4955,4956],{"class":99},"\"缺少 item_id 参数\"",[58,4958,4767],{"class":92},[58,4960,4770],{"class":382},[58,4962,4963],{"class":60,"line":174},[58,4964,451],{"class":92},[58,4966,4967],{"class":60,"line":180},[58,4968,4969],{"class":64},"    \u002F\u002F 查询并返回\n",[58,4971,4972,4974,4976,4978,4980,4983,4985],{"class":60,"line":185},[58,4973,267],{"class":71},[58,4975,4648],{"class":92},[58,4977,4759],{"class":75},[58,4979,273],{"class":92},[58,4981,4982],{"class":99},"\"项目详情...\"",[58,4984,4767],{"class":92},[58,4986,4770],{"class":382},[58,4988,4989],{"class":60,"line":191},[58,4990,3621],{"class":92},[44,4992,4994],{"id":4993},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[15,4996,4997,4998,5001,5002,5005],{},"关键一步：用 ",[19,4999,5000],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[19,5003,5004],{},"gin.WrapH"," 适配 Gin。",[49,5007,5009],{"className":51,"code":5008,"language":53,"meta":54,"style":54},"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",[19,5010,5011,5021,5025,5040,5054,5071,5075,5079,5084],{"__ignoreMap":54},[58,5012,5013,5015,5017,5019],{"class":60,"line":61},[58,5014,89],{"class":71},[58,5016,478],{"class":99},[58,5018,4513],{"class":75},[58,5020,106],{"class":99},[58,5022,5023],{"class":60,"line":68},[58,5024,83],{"emptyLinePlaceholder":82},[58,5026,5027,5030,5032,5034,5037],{"class":60,"line":79},[58,5028,5029],{"class":92},"mcpHTTPServer ",[58,5031,242],{"class":71},[58,5033,4529],{"class":92},[58,5035,5036],{"class":75},"NewStreamableHTTPServer",[58,5038,5039],{"class":92},"(mcpServer,\n",[58,5041,5042,5044,5047,5049,5052],{"class":60,"line":86},[58,5043,4562],{"class":92},[58,5045,5046],{"class":75},"WithEndpointPath",[58,5048,273],{"class":92},[58,5050,5051],{"class":99},"\"\u002Fmcp\"",[58,5053,4592],{"class":92},[58,5055,5056,5058,5061,5063,5066,5068],{"class":60,"line":96},[58,5057,4562],{"class":92},[58,5059,5060],{"class":75},"WithHeartbeatInterval",[58,5062,273],{"class":92},[58,5064,5065],{"class":382},"30",[58,5067,4730],{"class":71},[58,5069,5070],{"class":92},"time.Second),\n",[58,5072,5073],{"class":60,"line":109},[58,5074,177],{"class":92},[58,5076,5077],{"class":60,"line":119},[58,5078,83],{"emptyLinePlaceholder":82},[58,5080,5081],{"class":60,"line":129},[58,5082,5083],{"class":64},"\u002F\u002F 挂载到 Gin\n",[58,5085,5086,5089,5092,5094,5096,5099,5102],{"class":60,"line":139},[58,5087,5088],{"class":92},"r.",[58,5090,5091],{"class":75},"Any",[58,5093,273],{"class":92},[58,5095,5051],{"class":99},[58,5097,5098],{"class":92},", gin.",[58,5100,5101],{"class":75},"WrapH",[58,5103,5104],{"class":92},"(mcpHTTPServer))\n",[15,5106,5107,5108,5111],{},"就这样，",[19,5109,5110],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[44,5113,5115],{"id":5114},"_5-完整的服务启动代码","5. 完整的服务启动代码",[49,5117,5119],{"className":51,"code":5118,"language":53,"meta":54,"style":54},"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",[19,5120,5121,5130,5145,5149,5154,5177,5186,5194,5199,5203,5208,5216,5220,5225,5238,5250,5264,5268,5286,5290,5295,5324,5358,5363,5367,5381],{"__ignoreMap":54},[58,5122,5123,5125,5128],{"class":60,"line":61},[58,5124,216],{"class":71},[58,5126,5127],{"class":75}," main",[58,5129,2376],{"class":92},[58,5131,5132,5135,5137,5140,5143],{"class":60,"line":68},[58,5133,5134],{"class":92},"    r ",[58,5136,242],{"class":71},[58,5138,5139],{"class":92}," gin.",[58,5141,5142],{"class":75},"Default",[58,5144,508],{"class":92},[58,5146,5147],{"class":60,"line":79},[58,5148,83],{"emptyLinePlaceholder":82},[58,5150,5151],{"class":60,"line":86},[58,5152,5153],{"class":64},"    \u002F\u002F 创建 MCP 服务器\n",[58,5155,5156,5159,5161,5163,5165,5167,5170,5172,5175],{"class":60,"line":96},[58,5157,5158],{"class":92},"    mcpServer ",[58,5160,242],{"class":71},[58,5162,4529],{"class":92},[58,5164,4532],{"class":75},[58,5166,273],{"class":92},[58,5168,5169],{"class":99},"\"my-app\"",[58,5171,889],{"class":92},[58,5173,5174],{"class":99},"\"1.0.0\"",[58,5176,2741],{"class":92},[58,5178,5179,5182,5184],{"class":60,"line":109},[58,5180,5181],{"class":92},"        server.",[58,5183,4565],{"class":75},[58,5185,4568],{"class":92},[58,5187,5188,5190,5192],{"class":60,"line":119},[58,5189,5181],{"class":92},[58,5191,4575],{"class":75},[58,5193,4568],{"class":92},[58,5195,5196],{"class":60,"line":129},[58,5197,5198],{"class":92},"    )\n",[58,5200,5201],{"class":60,"line":139},[58,5202,83],{"emptyLinePlaceholder":82},[58,5204,5205],{"class":60,"line":149},[58,5206,5207],{"class":64},"    \u002F\u002F 注册工具\n",[58,5209,5210,5213],{"class":60,"line":159},[58,5211,5212],{"class":75},"    registerTools",[58,5214,5215],{"class":92},"(mcpServer)\n",[58,5217,5218],{"class":60,"line":164},[58,5219,83],{"emptyLinePlaceholder":82},[58,5221,5222],{"class":60,"line":174},[58,5223,5224],{"class":64},"    \u002F\u002F 挂载到 Gin\n",[58,5226,5227,5230,5232,5234,5236],{"class":60,"line":180},[58,5228,5229],{"class":92},"    mcpHTTP ",[58,5231,242],{"class":71},[58,5233,4529],{"class":92},[58,5235,5036],{"class":75},[58,5237,5039],{"class":92},[58,5239,5240,5242,5244,5246,5248],{"class":60,"line":185},[58,5241,5181],{"class":92},[58,5243,5046],{"class":75},[58,5245,273],{"class":92},[58,5247,5051],{"class":99},[58,5249,4592],{"class":92},[58,5251,5252,5254,5256,5258,5260,5262],{"class":60,"line":191},[58,5253,5181],{"class":92},[58,5255,5060],{"class":75},[58,5257,273],{"class":92},[58,5259,5065],{"class":382},[58,5261,4730],{"class":71},[58,5263,5070],{"class":92},[58,5265,5266],{"class":60,"line":208},[58,5267,5198],{"class":92},[58,5269,5270,5273,5275,5277,5279,5281,5283],{"class":60,"line":213},[58,5271,5272],{"class":92},"    r.",[58,5274,5091],{"class":75},[58,5276,273],{"class":92},[58,5278,5051],{"class":99},[58,5280,5098],{"class":92},[58,5282,5101],{"class":75},[58,5284,5285],{"class":92},"(mcpHTTP))\n",[58,5287,5288],{"class":60,"line":236},[58,5289,83],{"emptyLinePlaceholder":82},[58,5291,5292],{"class":60,"line":259},[58,5293,5294],{"class":64},"    \u002F\u002F 其他业务路由\n",[58,5296,5297,5299,5301,5303,5306,5308,5310,5312,5314,5316,5318,5320,5322],{"class":60,"line":264},[58,5298,5272],{"class":92},[58,5300,516],{"class":75},[58,5302,273],{"class":92},[58,5304,5305],{"class":99},"\"\u002Fapi\u002Fhealth\"",[58,5307,889],{"class":92},[58,5309,216],{"class":71},[58,5311,273],{"class":92},[58,5313,277],{"class":276},[58,5315,280],{"class":71},[58,5317,225],{"class":75},[58,5319,202],{"class":92},[58,5321,287],{"class":75},[58,5323,290],{"class":92},[58,5325,5326,5328,5331,5333,5336,5338,5340,5342,5345,5348,5351,5353,5356],{"class":60,"line":293},[58,5327,427],{"class":92},[58,5329,5330],{"class":75},"JSON",[58,5332,273],{"class":92},[58,5334,5335],{"class":382},"200",[58,5337,889],{"class":92},[58,5339,225],{"class":75},[58,5341,202],{"class":92},[58,5343,5344],{"class":75},"H",[58,5346,5347],{"class":92},"{",[58,5349,5350],{"class":99},"\"status\"",[58,5352,2794],{"class":92},[58,5354,5355],{"class":99},"\"ok\"",[58,5357,3621],{"class":92},[58,5359,5360],{"class":60,"line":315},[58,5361,5362],{"class":92},"    })\n",[58,5364,5365],{"class":60,"line":332},[58,5366,83],{"emptyLinePlaceholder":82},[58,5368,5369,5371,5374,5376,5379],{"class":60,"line":344},[58,5370,5272],{"class":92},[58,5372,5373],{"class":75},"Run",[58,5375,273],{"class":92},[58,5377,5378],{"class":99},"\":8080\"",[58,5380,177],{"class":92},[58,5382,5383],{"class":60,"line":350},[58,5384,457],{"class":92},[15,5386,5387],{},"此时启动服务，用 curl 测试：",[49,5389,5391],{"className":579,"code":5390,"language":581,"meta":54,"style":54},"# 发现工具列表\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",[19,5392,5393,5398,5414,5424,5432,5436,5441,5453,5461],{"__ignoreMap":54},[58,5394,5395],{"class":60,"line":61},[58,5396,5397],{"class":64},"# 发现工具列表\n",[58,5399,5400,5403,5406,5409,5412],{"class":60,"line":68},[58,5401,5402],{"class":75},"curl",[58,5404,5405],{"class":382}," -X",[58,5407,5408],{"class":99}," POST",[58,5410,5411],{"class":99}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[58,5413,702],{"class":382},[58,5415,5416,5419,5422],{"class":60,"line":79},[58,5417,5418],{"class":382},"  -H",[58,5420,5421],{"class":99}," \"Content-Type: application\u002Fjson\"",[58,5423,702],{"class":382},[58,5425,5426,5429],{"class":60,"line":86},[58,5427,5428],{"class":382},"  -d",[58,5430,5431],{"class":99}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[58,5433,5434],{"class":60,"line":96},[58,5435,83],{"emptyLinePlaceholder":82},[58,5437,5438],{"class":60,"line":109},[58,5439,5440],{"class":64},"# 调用工具\n",[58,5442,5443,5445,5447,5449,5451],{"class":60,"line":119},[58,5444,5402],{"class":75},[58,5446,5405],{"class":382},[58,5448,5408],{"class":99},[58,5450,5411],{"class":99},[58,5452,702],{"class":382},[58,5454,5455,5457,5459],{"class":60,"line":129},[58,5456,5418],{"class":382},[58,5458,5421],{"class":99},[58,5460,702],{"class":382},[58,5462,5463,5465],{"class":60,"line":139},[58,5464,5428],{"class":382},[58,5466,5467],{"class":99}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[11,5469,5471],{"id":5470},"拓展接入鉴权","拓展：接入鉴权",[15,5473,5474],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[44,5476,4048],{"id":4048},[15,5478,5479,5480,5483,5484,5487],{},"MCP 的工具处理器签名为 ",[19,5481,5482],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[19,5485,5486],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[44,5489,5491],{"id":5490},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[15,5493,5494,5495,782],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[19,5496,5497],{},"request.Context",[49,5499,5501],{"className":51,"code":5500,"language":53,"meta":54,"style":54},"\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",[19,5502,5503,5508,5535,5555,5575,5588,5621,5626,5630,5634,5658,5698,5727,5731,5735,5739,5759,5771,5800,5804,5808,5812,5827,5841,5850,5854],{"__ignoreMap":54},[58,5504,5505],{"class":60,"line":61},[58,5506,5507],{"class":64},"\u002F\u002F Gin 鉴权中间件\n",[58,5509,5510,5512,5515,5517,5520,5522,5525,5527,5529,5531,5533],{"class":60,"line":68},[58,5511,216],{"class":71},[58,5513,5514],{"class":75}," AuthRequired",[58,5516,273],{"class":92},[58,5518,5519],{"class":276},"authService",[58,5521,280],{"class":71},[58,5523,5524],{"class":75},"AuthService",[58,5526,3539],{"class":92},[58,5528,225],{"class":75},[58,5530,202],{"class":92},[58,5532,230],{"class":75},[58,5534,233],{"class":92},[58,5536,5537,5539,5541,5543,5545,5547,5549,5551,5553],{"class":60,"line":79},[58,5538,267],{"class":71},[58,5540,270],{"class":71},[58,5542,273],{"class":92},[58,5544,277],{"class":276},[58,5546,280],{"class":71},[58,5548,225],{"class":75},[58,5550,202],{"class":92},[58,5552,287],{"class":75},[58,5554,290],{"class":92},[58,5556,5557,5560,5562,5565,5568,5570,5573],{"class":60,"line":86},[58,5558,5559],{"class":92},"        authHeader ",[58,5561,242],{"class":71},[58,5563,5564],{"class":92}," c.",[58,5566,5567],{"class":75},"GetHeader",[58,5569,273],{"class":92},[58,5571,5572],{"class":99},"\"Authorization\"",[58,5574,177],{"class":92},[58,5576,5577,5579,5582,5584,5586],{"class":60,"line":96},[58,5578,318],{"class":71},[58,5580,5581],{"class":92}," authHeader ",[58,5583,324],{"class":71},[58,5585,327],{"class":99},[58,5587,233],{"class":92},[58,5589,5590,5593,5596,5598,5601,5603,5605,5607,5609,5611,5614,5616,5619],{"class":60,"line":109},[58,5591,5592],{"class":92},"            c.",[58,5594,5595],{"class":75},"AbortWithStatusJSON",[58,5597,273],{"class":92},[58,5599,5600],{"class":382},"401",[58,5602,889],{"class":92},[58,5604,225],{"class":75},[58,5606,202],{"class":92},[58,5608,5344],{"class":75},[58,5610,5347],{"class":92},[58,5612,5613],{"class":99},"\"message\"",[58,5615,2794],{"class":92},[58,5617,5618],{"class":99},"\"缺少认证令牌\"",[58,5620,3621],{"class":92},[58,5622,5623],{"class":60,"line":119},[58,5624,5625],{"class":71},"            return\n",[58,5627,5628],{"class":60,"line":129},[58,5629,347],{"class":92},[58,5631,5632],{"class":60,"line":139},[58,5633,83],{"emptyLinePlaceholder":82},[58,5635,5636,5639,5641,5643,5646,5649,5652,5654,5656],{"class":60,"line":149},[58,5637,5638],{"class":92},"        parts ",[58,5640,242],{"class":71},[58,5642,301],{"class":92},[58,5644,5645],{"class":75},"SplitN",[58,5647,5648],{"class":92},"(authHeader, ",[58,5650,5651],{"class":99},"\" \"",[58,5653,889],{"class":92},[58,5655,3263],{"class":382},[58,5657,177],{"class":92},[58,5659,5660,5662,5665,5668,5670,5673,5676,5679,5682,5685,5688,5690,5693,5696],{"class":60,"line":159},[58,5661,318],{"class":71},[58,5663,5664],{"class":75}," len",[58,5666,5667],{"class":92},"(parts) ",[58,5669,379],{"class":71},[58,5671,5672],{"class":382}," 2",[58,5674,5675],{"class":71}," ||",[58,5677,5678],{"class":71}," !",[58,5680,5681],{"class":92},"strings.",[58,5683,5684],{"class":75},"EqualFold",[58,5686,5687],{"class":92},"(parts[",[58,5689,3171],{"class":382},[58,5691,5692],{"class":92},"], ",[58,5694,5695],{"class":99},"\"Bearer\"",[58,5697,290],{"class":92},[58,5699,5700,5702,5704,5706,5708,5710,5712,5714,5716,5718,5720,5722,5725],{"class":60,"line":164},[58,5701,5592],{"class":92},[58,5703,5595],{"class":75},[58,5705,273],{"class":92},[58,5707,5600],{"class":382},[58,5709,889],{"class":92},[58,5711,225],{"class":75},[58,5713,202],{"class":92},[58,5715,5344],{"class":75},[58,5717,5347],{"class":92},[58,5719,5613],{"class":99},[58,5721,2794],{"class":92},[58,5723,5724],{"class":99},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[58,5726,3621],{"class":92},[58,5728,5729],{"class":60,"line":174},[58,5730,5625],{"class":71},[58,5732,5733],{"class":60,"line":180},[58,5734,347],{"class":92},[58,5736,5737],{"class":60,"line":185},[58,5738,83],{"emptyLinePlaceholder":82},[58,5740,5741,5744,5746,5749,5752,5754,5756],{"class":60,"line":191},[58,5742,5743],{"class":92},"        user, err ",[58,5745,242],{"class":71},[58,5747,5748],{"class":92}," authService.",[58,5750,5751],{"class":75},"GetUserByToken",[58,5753,5687],{"class":92},[58,5755,1229],{"class":382},[58,5757,5758],{"class":92},"])\n",[58,5760,5761,5763,5765,5767,5769],{"class":60,"line":208},[58,5762,318],{"class":71},[58,5764,376],{"class":92},[58,5766,379],{"class":71},[58,5768,383],{"class":382},[58,5770,233],{"class":92},[58,5772,5773,5775,5777,5779,5781,5783,5785,5787,5789,5791,5793,5795,5798],{"class":60,"line":213},[58,5774,5592],{"class":92},[58,5776,5595],{"class":75},[58,5778,273],{"class":92},[58,5780,5600],{"class":382},[58,5782,889],{"class":92},[58,5784,225],{"class":75},[58,5786,202],{"class":92},[58,5788,5344],{"class":75},[58,5790,5347],{"class":92},[58,5792,5613],{"class":99},[58,5794,2794],{"class":92},[58,5796,5797],{"class":99},"\"无效的认证令牌\"",[58,5799,3621],{"class":92},[58,5801,5802],{"class":60,"line":236},[58,5803,5625],{"class":71},[58,5805,5806],{"class":60,"line":259},[58,5807,347],{"class":92},[58,5809,5810],{"class":60,"line":264},[58,5811,83],{"emptyLinePlaceholder":82},[58,5813,5814,5816,5819,5821,5824],{"class":60,"line":293},[58,5815,427],{"class":92},[58,5817,5818],{"class":75},"Set",[58,5820,273],{"class":92},[58,5822,5823],{"class":99},"\"userID\"",[58,5825,5826],{"class":92},", user.ID)\n",[58,5828,5829,5831,5833,5835,5838],{"class":60,"line":315},[58,5830,427],{"class":92},[58,5832,5818],{"class":75},[58,5834,273],{"class":92},[58,5836,5837],{"class":99},"\"username\"",[58,5839,5840],{"class":92},", user.Username)\n",[58,5842,5843,5845,5848],{"class":60,"line":332},[58,5844,427],{"class":92},[58,5846,5847],{"class":75},"Next",[58,5849,508],{"class":92},[58,5851,5852],{"class":60,"line":344},[58,5853,451],{"class":92},[58,5855,5856],{"class":60,"line":350},[58,5857,457],{"class":92},[49,5859,5861],{"className":51,"code":5860,"language":53,"meta":54,"style":54},"\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",[19,5862,5863,5868,5879,5885,5898,5910,5914,5918,5935,5955,5969,5993,6015,6030,6038,6042],{"__ignoreMap":54},[58,5864,5865],{"class":60,"line":61},[58,5866,5867],{"class":64},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[58,5869,5870,5873,5876],{"class":60,"line":68},[58,5871,5872],{"class":71},"type",[58,5874,5875],{"class":75}," ctxKey",[58,5877,5878],{"class":71}," string\n",[58,5880,5881,5883],{"class":60,"line":79},[58,5882,3310],{"class":71},[58,5884,93],{"class":92},[58,5886,5887,5890,5893,5895],{"class":60,"line":86},[58,5888,5889],{"class":382},"    ctxUserID",[58,5891,5892],{"class":75},"   ctxKey",[58,5894,2728],{"class":71},[58,5896,5897],{"class":99}," \"userID\"\n",[58,5899,5900,5903,5905,5907],{"class":60,"line":96},[58,5901,5902],{"class":382},"    ctxUsername",[58,5904,5875],{"class":75},[58,5906,2728],{"class":71},[58,5908,5909],{"class":99}," \"username\"\n",[58,5911,5912],{"class":60,"line":109},[58,5913,177],{"class":92},[58,5915,5916],{"class":60,"line":119},[58,5917,83],{"emptyLinePlaceholder":82},[58,5919,5920,5922,5925,5927,5929,5931,5933],{"class":60,"line":129},[58,5921,216],{"class":71},[58,5923,5924],{"class":75}," WithUserContext",[58,5926,222],{"class":92},[58,5928,225],{"class":75},[58,5930,202],{"class":92},[58,5932,230],{"class":75},[58,5934,233],{"class":92},[58,5936,5937,5939,5941,5943,5945,5947,5949,5951,5953],{"class":60,"line":139},[58,5938,267],{"class":71},[58,5940,270],{"class":71},[58,5942,273],{"class":92},[58,5944,277],{"class":276},[58,5946,280],{"class":71},[58,5948,225],{"class":75},[58,5950,202],{"class":92},[58,5952,287],{"class":75},[58,5954,290],{"class":92},[58,5956,5957,5960,5962,5965,5967],{"class":60,"line":149},[58,5958,5959],{"class":92},"        ctx ",[58,5961,242],{"class":71},[58,5963,5964],{"class":92}," c.Request.",[58,5966,287],{"class":75},[58,5968,508],{"class":92},[58,5970,5971,5973,5975,5978,5981,5984,5987,5989,5991],{"class":60,"line":159},[58,5972,5959],{"class":92},[58,5974,338],{"class":71},[58,5976,5977],{"class":92}," context.",[58,5979,5980],{"class":75},"WithValue",[58,5982,5983],{"class":92},"(ctx, ctxUserID, c.",[58,5985,5986],{"class":75},"GetUint",[58,5988,273],{"class":92},[58,5990,5823],{"class":99},[58,5992,3240],{"class":92},[58,5994,5995,5997,5999,6001,6003,6006,6009,6011,6013],{"class":60,"line":164},[58,5996,5959],{"class":92},[58,5998,338],{"class":71},[58,6000,5977],{"class":92},[58,6002,5980],{"class":75},[58,6004,6005],{"class":92},"(ctx, ctxUsername, c.",[58,6007,6008],{"class":75},"GetString",[58,6010,273],{"class":92},[58,6012,5837],{"class":99},[58,6014,3240],{"class":92},[58,6016,6017,6020,6022,6024,6027],{"class":60,"line":174},[58,6018,6019],{"class":92},"        c.Request ",[58,6021,338],{"class":71},[58,6023,5964],{"class":92},[58,6025,6026],{"class":75},"WithContext",[58,6028,6029],{"class":92},"(ctx)\n",[58,6031,6032,6034,6036],{"class":60,"line":180},[58,6033,427],{"class":92},[58,6035,5847],{"class":75},[58,6037,508],{"class":92},[58,6039,6040],{"class":60,"line":185},[58,6041,451],{"class":92},[58,6043,6044],{"class":60,"line":191},[58,6045,457],{"class":92},[44,6047,6048],{"id":6048},"组合使用",[49,6050,6052],{"className":51,"code":6051,"language":53,"meta":54,"style":54},"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",[19,6053,6054,6066,6077,6088,6101],{"__ignoreMap":54},[58,6055,6056,6058,6060,6062,6064],{"class":60,"line":61},[58,6057,5088],{"class":92},[58,6059,5091],{"class":75},[58,6061,273],{"class":92},[58,6063,5051],{"class":99},[58,6065,2741],{"class":92},[58,6067,6068,6071,6074],{"class":60,"line":68},[58,6069,6070],{"class":75},"    AuthRequired",[58,6072,6073],{"class":92},"(authService),  ",[58,6075,6076],{"class":64},"\u002F\u002F 1. 验证 Token\n",[58,6078,6079,6082,6085],{"class":60,"line":79},[58,6080,6081],{"class":75},"    WithUserContext",[58,6083,6084],{"class":92},"(),           ",[58,6086,6087],{"class":64},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[58,6089,6090,6093,6095,6098],{"class":60,"line":86},[58,6091,6092],{"class":92},"    gin.",[58,6094,5101],{"class":75},[58,6096,6097],{"class":92},"(mcpHTTP),         ",[58,6099,6100],{"class":64},"\u002F\u002F 3. MCP 处理\n",[58,6102,6103],{"class":60,"line":96},[58,6104,177],{"class":92},[15,6106,6107],{},"在工具处理器里就能拿到用户信息了：",[49,6109,6111],{"className":51,"code":6110,"language":53,"meta":54,"style":54},"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",[19,6112,6113,6168,6189,6198,6215,6219,6223,6239,6244,6257],{"__ignoreMap":54},[58,6114,6115,6117,6119,6122,6124,6127,6129,6132,6134,6136,6138,6140,6142,6144,6146,6148,6150,6152,6154,6156,6158,6160,6162,6164,6166],{"class":60,"line":61},[58,6116,216],{"class":71},[58,6118,3125],{"class":92},[58,6120,6121],{"class":276},"h ",[58,6123,4730],{"class":71},[58,6125,6126],{"class":75},"Handler",[58,6128,3539],{"class":92},[58,6130,6131],{"class":75},"handleListItems",[58,6133,273],{"class":92},[58,6135,4704],{"class":276},[58,6137,4707],{"class":75},[58,6139,202],{"class":92},[58,6141,287],{"class":75},[58,6143,889],{"class":92},[58,6145,4716],{"class":276},[58,6147,4719],{"class":75},[58,6149,202],{"class":92},[58,6151,4724],{"class":75},[58,6153,4727],{"class":92},[58,6155,4730],{"class":71},[58,6157,4733],{"class":75},[58,6159,202],{"class":92},[58,6161,4738],{"class":75},[58,6163,889],{"class":92},[58,6165,4743],{"class":71},[58,6167,290],{"class":92},[58,6169,6170,6173,6175,6178,6181,6184,6187],{"class":60,"line":68},[58,6171,6172],{"class":92},"    userID, ok ",[58,6174,242],{"class":71},[58,6176,6177],{"class":92}," ctx.",[58,6179,6180],{"class":75},"Value",[58,6182,6183],{"class":92},"(ctxUserID).(",[58,6185,6186],{"class":71},"uint",[58,6188,177],{"class":92},[58,6190,6191,6193,6195],{"class":60,"line":79},[58,6192,3160],{"class":71},[58,6194,5678],{"class":71},[58,6196,6197],{"class":92},"ok {\n",[58,6199,6200,6202,6204,6206,6208,6211,6213],{"class":60,"line":86},[58,6201,857],{"class":71},[58,6203,4648],{"class":92},[58,6205,4951],{"class":75},[58,6207,273],{"class":92},[58,6209,6210],{"class":99},"\"未认证\"",[58,6212,4767],{"class":92},[58,6214,4770],{"class":382},[58,6216,6217],{"class":60,"line":96},[58,6218,451],{"class":92},[58,6220,6221],{"class":60,"line":109},[58,6222,83],{"emptyLinePlaceholder":82},[58,6224,6225,6228,6230,6233,6236],{"class":60,"line":119},[58,6226,6227],{"class":92},"    items, _ ",[58,6229,242],{"class":71},[58,6231,6232],{"class":92}," h.service.",[58,6234,6235],{"class":75},"ListByUser",[58,6237,6238],{"class":92},"(userID)\n",[58,6240,6241],{"class":60,"line":129},[58,6242,6243],{"class":64},"    \u002F\u002F ...\n",[58,6245,6246,6248,6250,6252,6255],{"class":60,"line":139},[58,6247,267],{"class":71},[58,6249,4648],{"class":92},[58,6251,4759],{"class":75},[58,6253,6254],{"class":92},"(result), ",[58,6256,4770],{"class":382},[58,6258,6259],{"class":60,"line":149},[58,6260,457],{"class":92},[44,6262,6263],{"id":6263},"客户端配置",[15,6265,6266],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[49,6268,6272],{"className":6269,"code":6270,"language":6271,"meta":54,"style":54},"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",[19,6273,6274,6279,6286,6292,6304,6311,6321,6326,6330,6334],{"__ignoreMap":54},[58,6275,6276],{"class":60,"line":61},[58,6277,6278],{"class":92},"{\n",[58,6280,6281,6284],{"class":60,"line":68},[58,6282,6283],{"class":382},"  \"mcpServers\"",[58,6285,830],{"class":92},[58,6287,6288,6290],{"class":60,"line":79},[58,6289,4540],{"class":382},[58,6291,830],{"class":92},[58,6293,6294,6297,6299,6302],{"class":60,"line":86},[58,6295,6296],{"class":382},"      \"url\"",[58,6298,2794],{"class":92},[58,6300,6301],{"class":99},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[58,6303,2741],{"class":92},[58,6305,6306,6309],{"class":60,"line":96},[58,6307,6308],{"class":382},"      \"headers\"",[58,6310,830],{"class":92},[58,6312,6313,6316,6318],{"class":60,"line":109},[58,6314,6315],{"class":382},"        \"Authorization\"",[58,6317,2794],{"class":92},[58,6319,6320],{"class":99},"\"Bearer your-token-here\"\n",[58,6322,6323],{"class":60,"line":119},[58,6324,6325],{"class":92},"      }\n",[58,6327,6328],{"class":60,"line":129},[58,6329,451],{"class":92},[58,6331,6332],{"class":60,"line":139},[58,6333,3050],{"class":92},[58,6335,6336],{"class":60,"line":149},[58,6337,457],{"class":92},[11,6339,1041],{"id":1041},[995,6341,6342,6352],{},[998,6343,6344],{},[1001,6345,6346,6349],{},[1004,6347,6348],{},"步骤",[1004,6350,6351],{},"说明",[1014,6353,6354,6363,6374,6383,6392],{},[1001,6355,6356,6360],{},[1019,6357,6358],{},[19,6359,4532],{},[1019,6361,6362],{},"创建 MCP 协议实例",[1001,6364,6365,6371],{},[1019,6366,6367,2265,6369],{},[19,6368,4651],{},[19,6370,4694],{},[1019,6372,6373],{},"定义并注册工具",[1001,6375,6376,6380],{},[1019,6377,6378],{},[19,6379,5036],{},[1019,6381,6382],{},"转为 HTTP Handler",[1001,6384,6385,6389],{},[1019,6386,6387],{},[19,6388,5004],{},[1019,6390,6391],{},"挂载到 Gin 路由",[1001,6393,6394,6402],{},[1019,6395,6396,2265,6399],{},[19,6397,6398],{},"AuthRequired",[19,6400,6401],{},"WithUserContext",[1019,6403,6404],{},"鉴权 + 用户信息桥接",[15,6406,6407,6408,6411,6412,782],{},"核心就一句话：",[737,6409,6410],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[19,6413,5497],{},[1066,6415,6416],{},"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":54,"searchDepth":68,"depth":79,"links":6418},[6419,6420,6421,6422,6429,6435],{"id":4414,"depth":68,"text":4415},{"id":4424,"depth":68,"text":4425},{"id":4441,"depth":68,"text":4441},{"id":4467,"depth":68,"text":4467,"children":6423},[6424,6425,6426,6427,6428],{"id":4470,"depth":79,"text":4471},{"id":4498,"depth":79,"text":4499},{"id":4612,"depth":79,"text":4613},{"id":4993,"depth":79,"text":4994},{"id":5114,"depth":79,"text":5115},{"id":5470,"depth":68,"text":5471,"children":6430},[6431,6432,6433,6434],{"id":4048,"depth":79,"text":4048},{"id":5490,"depth":79,"text":5491},{"id":6048,"depth":79,"text":6048},{"id":6263,"depth":79,"text":6263},{"id":1041,"depth":68,"text":1041},"介绍如何使用 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":4409,"description":6436},"posts\u002Fgo-mcp-gin\u002Findex",[1098,6444,6445,6446,6447],"MCP","Gin","AI Agent","后端","2026-06-01","5fZl1Vigs2lHb5fHPz-0YZFyFD38ws1heOouZUG1g6M",{"id":6451,"title":6452,"author":6,"body":6453,"description":7220,"difficulty":6,"draft":1088,"extension":1089,"image":7221,"meta":7222,"navigation":82,"path":7223,"pinned":1088,"published":7224,"seo":7225,"series":6,"seriesOrder":6,"stem":7226,"tags":7227,"updated":6,"__hash__":7231},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":8,"value":6454,"toc":7206},[6455,6459,6462,6469,6595,6598,6601,6657,6660,6675,6678,6681,6688,6920,6927,6931,6934,6954,6957,6972,6975,6995,6998,7001,7008,7074,7077,7080,7084,7087,7139,7143,7164,7168,7171,7185,7188,7192,7195,7197,7200,7203],[11,6456,6458],{"id":6457},"dws-是什么","dws 是什么",[15,6460,6461],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[15,6463,6464,6465,6468],{},"它的定位很明确：",[737,6466,6467],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[49,6470,6472],{"className":579,"code":6471,"language":581,"meta":54,"style":54},"# 查看今天有什么会\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",[19,6473,6474,6479,6505,6509,6514,6536,6540,6545,6570,6574,6579],{"__ignoreMap":54},[58,6475,6476],{"class":60,"line":61},[58,6477,6478],{"class":64},"# 查看今天有什么会\n",[58,6480,6481,6484,6487,6490,6493,6496,6499,6502],{"class":60,"line":68},[58,6482,6483],{"class":75},"dws",[58,6485,6486],{"class":99}," calendar",[58,6488,6489],{"class":99}," event",[58,6491,6492],{"class":99}," list",[58,6494,6495],{"class":382}," --start",[58,6497,6498],{"class":99}," 2026-06-01",[58,6500,6501],{"class":382}," --end",[58,6503,6504],{"class":99}," 2026-06-01\n",[58,6506,6507],{"class":60,"line":79},[58,6508,83],{"emptyLinePlaceholder":82},[58,6510,6511],{"class":60,"line":86},[58,6512,6513],{"class":64},"# 给群里发条消息\n",[58,6515,6516,6518,6521,6524,6527,6530,6533],{"class":60,"line":96},[58,6517,6483],{"class":75},[58,6519,6520],{"class":99}," chat",[58,6522,6523],{"class":99}," send",[58,6525,6526],{"class":382}," --group-id",[58,6528,6529],{"class":99}," xxx",[58,6531,6532],{"class":382}," --content",[58,6534,6535],{"class":99}," \"今天发布推迟到下午3点\"\n",[58,6537,6538],{"class":60,"line":109},[58,6539,83],{"emptyLinePlaceholder":82},[58,6541,6542],{"class":60,"line":119},[58,6543,6544],{"class":64},"# 创建一个待办\n",[58,6546,6547,6549,6552,6555,6558,6561,6564,6567],{"class":60,"line":129},[58,6548,6483],{"class":75},[58,6550,6551],{"class":99}," todo",[58,6553,6554],{"class":99}," task",[58,6556,6557],{"class":99}," create",[58,6559,6560],{"class":382}," --subject",[58,6562,6563],{"class":99}," \"Review PR #123\"",[58,6565,6566],{"class":382}," --due-date",[58,6568,6569],{"class":99}," 2026-06-02\n",[58,6571,6572],{"class":60,"line":139},[58,6573,83],{"emptyLinePlaceholder":82},[58,6575,6576],{"class":60,"line":149},[58,6577,6578],{"class":64},"# 读取钉钉文档\n",[58,6580,6581,6583,6586,6589,6592],{"class":60,"line":159},[58,6582,6483],{"class":75},[58,6584,6585],{"class":99}," doc",[58,6587,6588],{"class":99}," read",[58,6590,6591],{"class":382}," --url",[58,6593,6594],{"class":99}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[11,6596,6597],{"id":6597},"安装",[15,6599,6600],{},"三种方式，选你喜欢的：",[49,6602,6604],{"className":579,"code":6603,"language":581,"meta":54,"style":54},"# 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",[19,6605,6606,6611,6624,6628,6633,6643,6647,6652],{"__ignoreMap":54},[58,6607,6608],{"class":60,"line":61},[58,6609,6610],{"class":64},"# npm（推荐，跨平台自动下载二进制）\n",[58,6612,6613,6616,6618,6621],{"class":60,"line":68},[58,6614,6615],{"class":75},"npm",[58,6617,622],{"class":99},[58,6619,6620],{"class":382}," -g",[58,6622,6623],{"class":99}," dingtalk-workspace-cli\n",[58,6625,6626],{"class":60,"line":79},[58,6627,83],{"emptyLinePlaceholder":82},[58,6629,6630],{"class":60,"line":86},[58,6631,6632],{"class":64},"# Homebrew（macOS\u002FLinux）\n",[58,6634,6635,6638,6640],{"class":60,"line":96},[58,6636,6637],{"class":75},"brew",[58,6639,622],{"class":99},[58,6641,6642],{"class":99}," dws\n",[58,6644,6645],{"class":60,"line":109},[58,6646,83],{"emptyLinePlaceholder":82},[58,6648,6649],{"class":60,"line":119},[58,6650,6651],{"class":64},"# 直接下载二进制\n",[58,6653,6654],{"class":60,"line":129},[58,6655,6656],{"class":64},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[15,6658,6659],{},"安装后认证：",[49,6661,6663],{"className":579,"code":6662,"language":581,"meta":54,"style":54},"dws auth login\n",[19,6664,6665],{"__ignoreMap":54},[58,6666,6667,6669,6672],{"class":60,"line":61},[58,6668,6483],{"class":75},[58,6670,6671],{"class":99}," auth",[58,6673,6674],{"class":99}," login\n",[15,6676,6677],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[11,6679,6680],{"id":6680},"覆盖的钉钉产品",[15,6682,6683,6684,6687],{},"这是最夸张的部分 — ",[737,6685,6686],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[995,6689,6690,6702],{},[998,6691,6692],{},[1001,6693,6694,6697,6700],{},[1004,6695,6696],{},"产品",[1004,6698,6699],{},"命令数",[1004,6701,6351],{},[1014,6703,6704,6718,6732,6746,6760,6774,6788,6802,6815,6829,6842,6856,6870,6883,6896,6909],{},[1001,6705,6706,6712,6715],{},[1019,6707,6708,6709],{},"聊天 ",[19,6710,6711],{},"chat",[1019,6713,6714],{},"57",[1019,6716,6717],{},"消息收发、群管理、机器人、已读状态、Webhook",[1001,6719,6720,6726,6729],{},[1019,6721,6722,6723],{},"AI 表格 ",[19,6724,6725],{},"aitable",[1019,6727,6728],{},"42",[1019,6730,6731],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[1001,6733,6734,6740,6743],{},[1019,6735,6736,6737],{},"电子表格 ",[19,6738,6739],{},"sheet",[1019,6741,6742],{},"34",[1019,6744,6745],{},"工作表读写、行列操作、合并、筛选视图、导出",[1001,6747,6748,6754,6757],{},[1019,6749,6750,6751],{},"文档 ",[19,6752,6753],{},"doc",[1019,6755,6756],{},"21",[1019,6758,6759],{},"搜索、读写、块级编辑、评论、复制移动",[1001,6761,6762,6768,6771],{},[1019,6763,6764,6765],{},"会议纪要 ",[19,6766,6767],{},"minutes",[1019,6769,6770],{},"19",[1019,6772,6773],{},"列表、摘要、转写、思维导图、发言人、热词",[1001,6775,6776,6782,6785],{},[1019,6777,6778,6779],{},"日历 ",[19,6780,6781],{},"calendar",[1019,6783,6784],{},"14",[1019,6786,6787],{},"事件 CRUD、会议室、忙闲查询、时间建议",[1001,6789,6790,6796,6799],{},[1019,6791,6792,6793],{},"审批 ",[19,6794,6795],{},"oa",[1019,6797,6798],{},"9",[1019,6800,6801],{},"待办、发起、同意\u002F拒绝\u002F撤销",[1001,6803,6804,6810,6812],{},[1019,6805,6806,6807],{},"云盘 ",[19,6808,6809],{},"drive",[1019,6811,6798],{},[1019,6813,6814],{},"列表、下载、上传、创建目录",[1001,6816,6817,6823,6826],{},[1019,6818,6819,6820],{},"知识库 ",[19,6821,6822],{},"wiki",[1019,6824,6825],{},"7",[1019,6827,6828],{},"空间管理、成员管理",[1001,6830,6831,6837,6839],{},[1019,6832,6833,6834],{},"汇报 ",[19,6835,6836],{},"report",[1019,6838,6825],{},[1019,6840,6841],{},"日志\u002F日报\u002F周报，按模板创建、统计",[1001,6843,6844,6850,6853],{},[1019,6845,6846,6847],{},"待办 ",[19,6848,6849],{},"todo",[1019,6851,6852],{},"6",[1019,6854,6855],{},"创建、完成、删除，支持优先级和循环",[1001,6857,6858,6864,6867],{},[1019,6859,6860,6861],{},"邮件 ",[19,6862,6863],{},"mail",[1019,6865,6866],{},"4",[1019,6868,6869],{},"邮箱查询、邮件搜索、发送",[1001,6871,6872,6878,6880],{},[1019,6873,6874,6875],{},"考勤 ",[19,6876,6877],{},"attendance",[1019,6879,6866],{},[1019,6881,6882],{},"打卡记录、排班、汇总",[1001,6884,6885,6891,6893],{},[1019,6886,6887,6888],{},"通讯录 ",[19,6889,6890],{},"contact",[1019,6892,3035],{},[1019,6894,6895],{},"用户搜索、部门查询",[1001,6897,6898,6904,6906],{},[1019,6899,6900,6901],{},"DING 消息 ",[19,6902,6903],{},"ding",[1019,6905,3263],{},[1019,6907,6908],{},"发送\u002F撤回（应用内、短信、电话）",[1001,6910,6911,6914,6917],{},[1019,6912,6913],{},"其他",[1019,6915,6916],{},"若干",[1019,6918,6919],{},"AI 搜索、AI 应用、直播、开放平台文档",[15,6921,6922,6923,6926],{},"还有一个万能命令 ",[19,6924,6925],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[11,6928,6930],{"id":6929},"agent-skill-系统","Agent Skill 系统",[15,6932,6933],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[2638,6935,6936,6942,6948],{},[1052,6937,6938,6941],{},[737,6939,6940],{},"Claude Code"," — 通过 SKILL.md 自动加载",[1052,6943,6944,6947],{},[737,6945,6946],{},"Cursor"," — 支持 MCP 协议接入",[1052,6949,6950,6953],{},[737,6951,6952],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[15,6955,6956],{},"安装 skill：",[49,6958,6960],{"className":579,"code":6959,"language":581,"meta":54,"style":54},"dws skill setup\n",[19,6961,6962],{"__ignoreMap":54},[58,6963,6964,6966,6969],{"class":60,"line":61},[58,6965,6483],{"class":75},[58,6967,6968],{"class":99}," skill",[58,6970,6971],{"class":99}," setup\n",[15,6973,6974],{},"SKILL.md 里定义了：",[2638,6976,6977,6986,6989,6992],{},[1052,6978,6979,6980,6982,6983,6985],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[19,6981,6725],{},"，说\"打卡\"→ ",[19,6984,6877],{},"）",[1052,6987,6988],{},"危险操作确认流程（删除、撤回等必须先确认）",[1052,6990,6991],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[1052,6993,6994],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[15,6996,6997],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[11,6999,7000],{"id":7000},"自动升级",[15,7002,7003,7004,7007],{},"内置 ",[19,7005,7006],{},"dws upgrade","，不需要重新下载安装包：",[49,7009,7011],{"className":579,"code":7010,"language":581,"meta":54,"style":54},"dws upgrade              # 交互式升级到最新版\ndws upgrade --check      # 仅检查是否有新版本\ndws upgrade --list       # 列出最近版本\ndws upgrade --version v1.0.5  # 升级到指定版本\ndws upgrade --rollback   # 回滚到上一版本\n",[19,7012,7013,7023,7035,7047,7062],{"__ignoreMap":54},[58,7014,7015,7017,7020],{"class":60,"line":61},[58,7016,6483],{"class":75},[58,7018,7019],{"class":99}," upgrade",[58,7021,7022],{"class":64},"              # 交互式升级到最新版\n",[58,7024,7025,7027,7029,7032],{"class":60,"line":68},[58,7026,6483],{"class":75},[58,7028,7019],{"class":99},[58,7030,7031],{"class":382}," --check",[58,7033,7034],{"class":64},"      # 仅检查是否有新版本\n",[58,7036,7037,7039,7041,7044],{"class":60,"line":79},[58,7038,6483],{"class":75},[58,7040,7019],{"class":99},[58,7042,7043],{"class":382}," --list",[58,7045,7046],{"class":64},"       # 列出最近版本\n",[58,7048,7049,7051,7053,7056,7059],{"class":60,"line":86},[58,7050,6483],{"class":75},[58,7052,7019],{"class":99},[58,7054,7055],{"class":382}," --version",[58,7057,7058],{"class":99}," v1.0.5",[58,7060,7061],{"class":64},"  # 升级到指定版本\n",[58,7063,7064,7066,7068,7071],{"class":60,"line":96},[58,7065,6483],{"class":75},[58,7067,7019],{"class":99},[58,7069,7070],{"class":382}," --rollback",[58,7072,7073],{"class":64},"   # 回滚到上一版本\n",[15,7075,7076],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[11,7078,7079],{"id":7079},"亮点",[44,7081,7083],{"id":7082},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[15,7085,7086],{},"这可能是目前对 AI 最友好的 CLI 工具：",[2638,7088,7089,7110,7120,7129],{},[1052,7090,7091,7094,7095,7098,7099,7102,7103,7098,7106,7109],{},[737,7092,7093],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[19,7096,7097],{},"--userId"," 应该是 ",[19,7100,7101],{},"--user-id","，",[19,7104,7105],{},"--limit100",[19,7107,7108],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[1052,7111,7112,7115,7116,7119],{},[737,7113,7114],{},"Schema 发现"," — ",[19,7117,7118],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[1052,7121,7122,7115,7125,7128],{},[737,7123,7124],{},"jq 过滤",[19,7126,7127],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[1052,7130,7131,7134,7135,7138],{},[737,7132,7133],{},"JSON 输出"," — 所有命令支持 ",[19,7136,7137],{},"--format json","，方便程序解析",[44,7140,7142],{"id":7141},"_2-安全设计","2. 安全设计",[2638,7144,7145,7148,7151,7154,7161],{},[1052,7146,7147],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[1052,7149,7150],{},"域名白名单，只能访问钉钉官方 API",[1052,7152,7153],{},"强制 HTTPS",[1052,7155,7156,7157,7160],{},"写操作必须显式 ",[19,7158,7159],{},"--yes"," 确认",[1052,7162,7163],{},"完整审计链",[44,7165,7167],{"id":7166},"_3-发现驱动架构","3. 发现驱动架构",[15,7169,7170],{},"不是把所有命令硬编码在代码里，而是：",[1049,7172,7173,7176,7179,7182],{},[1052,7174,7175],{},"从钉钉 MCP 服务器注册中心拉取元数据",[1052,7177,7178],{},"运行时发现服务端能力",[1052,7180,7181],{},"转换成统一的中间表示（IR）",[1052,7183,7184],{},"动态挂载到命令树",[15,7186,7187],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[44,7189,7191],{"id":7190},"_4-多平台分发","4. 多平台分发",[15,7193,7194],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[11,7196,1041],{"id":1041},[15,7198,7199],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[15,7201,7202],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[1066,7204,7205],{},"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":54,"searchDepth":68,"depth":79,"links":7207},[7208,7209,7210,7211,7212,7213,7219],{"id":6457,"depth":68,"text":6458},{"id":6597,"depth":68,"text":6597},{"id":6680,"depth":68,"text":6680},{"id":6929,"depth":68,"text":6930},{"id":7000,"depth":68,"text":7000},{"id":7079,"depth":68,"text":7079,"children":7214},[7215,7216,7217,7218],{"id":7082,"depth":79,"text":7083},{"id":7141,"depth":79,"text":7142},{"id":7166,"depth":79,"text":7167},{"id":7190,"depth":79,"text":7191},{"id":1041,"depth":68,"text":1041},"介绍阿里巴巴官方的 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":6452,"description":7220},"posts\u002Fdws-dingtalk-cli\u002Findex",[7228,7229,6446,1098,7230],"钉钉","CLI","效率工具","4ca65VUZ2Sz9yG8dNtftrBDDHE6QowA3ed9g4egQkKI",{"id":4,"title":5,"author":6,"body":7233,"description":1086,"difficulty":1087,"draft":1088,"extension":1089,"image":1090,"meta":8011,"navigation":82,"path":1092,"pinned":1088,"published":1093,"seo":8012,"series":1095,"seriesOrder":68,"stem":1096,"tags":8013,"updated":1102,"__hash__":1103},{"type":8,"value":7234,"toc":7995},[7235,7237,7241,7243,7245,7251,7253,7255,7543,7545,7639,7641,7751,7753,7755,7761,7771,7777,7789,7801,7805,7811,7893,7897,7899,7903,7909,7911,7939,7941,7945,7975,7977,7981,7991,7993],[11,7236,13],{"id":13},[15,7238,17,7239,22],{},[19,7240,21],{},[15,7242,25],{},[11,7244,28],{"id":28},[15,7246,31,7247,35,7249,39],{},[19,7248,34],{},[19,7250,38],{},[11,7252,42],{"id":42},[44,7254,47],{"id":46},[49,7256,7257],{"className":51,"code":52,"language":53,"meta":54,"style":54},[19,7258,7259,7263,7269,7273,7279,7287,7295,7303,7311,7319,7327,7331,7339,7343,7347,7351,7363,7367,7383,7399,7403,7423,7439,7451,7459,7463,7467,7479,7491,7495,7511,7515,7519,7535,7539],{"__ignoreMap":54},[58,7260,7261],{"class":60,"line":61},[58,7262,65],{"class":64},[58,7264,7265,7267],{"class":60,"line":68},[58,7266,72],{"class":71},[58,7268,76],{"class":75},[58,7270,7271],{"class":60,"line":79},[58,7272,83],{"emptyLinePlaceholder":82},[58,7274,7275,7277],{"class":60,"line":86},[58,7276,89],{"class":71},[58,7278,93],{"class":92},[58,7280,7281,7283,7285],{"class":60,"line":96},[58,7282,100],{"class":99},[58,7284,103],{"class":75},[58,7286,106],{"class":99},[58,7288,7289,7291,7293],{"class":60,"line":109},[58,7290,100],{"class":99},[58,7292,114],{"class":75},[58,7294,106],{"class":99},[58,7296,7297,7299,7301],{"class":60,"line":119},[58,7298,100],{"class":99},[58,7300,124],{"class":75},[58,7302,106],{"class":99},[58,7304,7305,7307,7309],{"class":60,"line":129},[58,7306,100],{"class":99},[58,7308,134],{"class":75},[58,7310,106],{"class":99},[58,7312,7313,7315,7317],{"class":60,"line":139},[58,7314,100],{"class":99},[58,7316,144],{"class":75},[58,7318,106],{"class":99},[58,7320,7321,7323,7325],{"class":60,"line":149},[58,7322,100],{"class":99},[58,7324,154],{"class":75},[58,7326,106],{"class":99},[58,7328,7329],{"class":60,"line":159},[58,7330,83],{"emptyLinePlaceholder":82},[58,7332,7333,7335,7337],{"class":60,"line":164},[58,7334,100],{"class":99},[58,7336,169],{"class":75},[58,7338,106],{"class":99},[58,7340,7341],{"class":60,"line":174},[58,7342,177],{"class":92},[58,7344,7345],{"class":60,"line":180},[58,7346,83],{"emptyLinePlaceholder":82},[58,7348,7349],{"class":60,"line":185},[58,7350,188],{"class":64},[58,7352,7353,7355,7357,7359,7361],{"class":60,"line":191},[58,7354,194],{"class":71},[58,7356,197],{"class":92},[58,7358,103],{"class":75},[58,7360,202],{"class":92},[58,7362,205],{"class":75},[58,7364,7365],{"class":60,"line":208},[58,7366,83],{"emptyLinePlaceholder":82},[58,7368,7369,7371,7373,7375,7377,7379,7381],{"class":60,"line":213},[58,7370,216],{"class":71},[58,7372,219],{"class":75},[58,7374,222],{"class":92},[58,7376,225],{"class":75},[58,7378,202],{"class":92},[58,7380,230],{"class":75},[58,7382,233],{"class":92},[58,7384,7385,7387,7389,7391,7393,7395,7397],{"class":60,"line":236},[58,7386,239],{"class":92},[58,7388,242],{"class":71},[58,7390,245],{"class":92},[58,7392,248],{"class":75},[58,7394,251],{"class":92},[58,7396,254],{"class":99},[58,7398,177],{"class":92},[58,7400,7401],{"class":60,"line":259},[58,7402,83],{"emptyLinePlaceholder":82},[58,7404,7405,7407,7409,7411,7413,7415,7417,7419,7421],{"class":60,"line":264},[58,7406,267],{"class":71},[58,7408,270],{"class":71},[58,7410,273],{"class":92},[58,7412,277],{"class":276},[58,7414,280],{"class":71},[58,7416,225],{"class":75},[58,7418,202],{"class":92},[58,7420,287],{"class":75},[58,7422,290],{"class":92},[58,7424,7425,7427,7429,7431,7433,7435,7437],{"class":60,"line":293},[58,7426,296],{"class":92},[58,7428,242],{"class":71},[58,7430,301],{"class":92},[58,7432,304],{"class":75},[58,7434,307],{"class":92},[58,7436,310],{"class":99},[58,7438,177],{"class":92},[58,7440,7441,7443,7445,7447,7449],{"class":60,"line":315},[58,7442,318],{"class":71},[58,7444,321],{"class":92},[58,7446,324],{"class":71},[58,7448,327],{"class":99},[58,7450,233],{"class":92},[58,7452,7453,7455,7457],{"class":60,"line":332},[58,7454,335],{"class":92},[58,7456,338],{"class":71},[58,7458,341],{"class":99},[58,7460,7461],{"class":60,"line":344},[58,7462,347],{"class":92},[58,7464,7465],{"class":60,"line":350},[58,7466,83],{"emptyLinePlaceholder":82},[58,7468,7469,7471,7473,7475,7477],{"class":60,"line":355},[58,7470,358],{"class":92},[58,7472,242],{"class":71},[58,7474,245],{"class":92},[58,7476,365],{"class":75},[58,7478,368],{"class":92},[58,7480,7481,7483,7485,7487,7489],{"class":60,"line":371},[58,7482,318],{"class":71},[58,7484,376],{"class":92},[58,7486,379],{"class":71},[58,7488,383],{"class":382},[58,7490,233],{"class":92},[58,7492,7493],{"class":60,"line":388},[58,7494,391],{"class":64},[58,7496,7497,7499,7501,7503,7505,7507,7509],{"class":60,"line":394},[58,7498,397],{"class":92},[58,7500,338],{"class":71},[58,7502,245],{"class":92},[58,7504,365],{"class":75},[58,7506,406],{"class":92},[58,7508,409],{"class":99},[58,7510,177],{"class":92},[58,7512,7513],{"class":60,"line":414},[58,7514,347],{"class":92},[58,7516,7517],{"class":60,"line":419},[58,7518,83],{"emptyLinePlaceholder":82},[58,7520,7521,7523,7525,7527,7529,7531,7533],{"class":60,"line":424},[58,7522,427],{"class":92},[58,7524,430],{"class":75},[58,7526,433],{"class":92},[58,7528,436],{"class":75},[58,7530,439],{"class":92},[58,7532,442],{"class":75},[58,7534,445],{"class":92},[58,7536,7537],{"class":60,"line":448},[58,7538,451],{"class":92},[58,7540,7541],{"class":60,"line":454},[58,7542,457],{"class":92},[44,7544,461],{"id":460},[49,7546,7547],{"className":51,"code":464,"language":53,"meta":54,"style":54},[19,7548,7549,7553,7563,7567,7571,7583,7595,7607,7619,7631],{"__ignoreMap":54},[58,7550,7551],{"class":60,"line":61},[58,7552,471],{"class":64},[58,7554,7555,7557,7559,7561],{"class":60,"line":68},[58,7556,89],{"class":71},[58,7558,478],{"class":99},[58,7560,481],{"class":75},[58,7562,106],{"class":99},[58,7564,7565],{"class":60,"line":79},[58,7566,83],{"emptyLinePlaceholder":82},[58,7568,7569],{"class":60,"line":86},[58,7570,492],{"class":64},[58,7572,7573,7575,7577,7579,7581],{"class":60,"line":96},[58,7574,497],{"class":92},[58,7576,242],{"class":71},[58,7578,502],{"class":92},[58,7580,505],{"class":75},[58,7582,508],{"class":92},[58,7584,7585,7587,7589,7591,7593],{"class":60,"line":109},[58,7586,513],{"class":92},[58,7588,516],{"class":75},[58,7590,273],{"class":92},[58,7592,310],{"class":99},[58,7594,523],{"class":92},[58,7596,7597,7599,7601,7603,7605],{"class":60,"line":119},[58,7598,513],{"class":92},[58,7600,516],{"class":75},[58,7602,273],{"class":92},[58,7604,534],{"class":99},[58,7606,523],{"class":92},[58,7608,7609,7611,7613,7615,7617],{"class":60,"line":129},[58,7610,513],{"class":92},[58,7612,516],{"class":75},[58,7614,273],{"class":92},[58,7616,547],{"class":99},[58,7618,523],{"class":92},[58,7620,7621,7623,7625,7627,7629],{"class":60,"line":139},[58,7622,513],{"class":92},[58,7624,516],{"class":75},[58,7626,273],{"class":92},[58,7628,560],{"class":99},[58,7630,523],{"class":92},[58,7632,7633,7635,7637],{"class":60,"line":149},[58,7634,513],{"class":92},[58,7636,569],{"class":75},[58,7638,572],{"class":92},[44,7640,576],{"id":575},[49,7642,7643],{"className":579,"code":580,"language":581,"meta":54,"style":54},[19,7644,7645,7649,7653,7657,7663,7667,7671,7679,7687,7691,7695,7703,7713,7717,7721,7727,7739,7747],{"__ignoreMap":54},[58,7646,7647],{"class":60,"line":61},[58,7648,588],{"class":64},[58,7650,7651],{"class":60,"line":68},[58,7652,593],{"class":64},[58,7654,7655],{"class":60,"line":79},[58,7656,83],{"emptyLinePlaceholder":82},[58,7658,7659,7661],{"class":60,"line":86},[58,7660,602],{"class":382},[58,7662,605],{"class":382},[58,7664,7665],{"class":60,"line":96},[58,7666,83],{"emptyLinePlaceholder":82},[58,7668,7669],{"class":60,"line":109},[58,7670,614],{"class":64},[58,7672,7673,7675,7677],{"class":60,"line":119},[58,7674,619],{"class":75},[58,7676,622],{"class":99},[58,7678,625],{"class":382},[58,7680,7681,7683,7685],{"class":60,"line":129},[58,7682,630],{"class":75},[58,7684,633],{"class":99},[58,7686,636],{"class":99},[58,7688,7689],{"class":60,"line":139},[58,7690,83],{"emptyLinePlaceholder":82},[58,7692,7693],{"class":60,"line":149},[58,7694,645],{"class":64},[58,7696,7697,7699,7701],{"class":60,"line":159},[58,7698,650],{"class":75},[58,7700,653],{"class":382},[58,7702,656],{"class":99},[58,7704,7705,7707,7709,7711],{"class":60,"line":164},[58,7706,661],{"class":75},[58,7708,664],{"class":382},[58,7710,667],{"class":99},[58,7712,656],{"class":99},[58,7714,7715],{"class":60,"line":174},[58,7716,83],{"emptyLinePlaceholder":82},[58,7718,7719],{"class":60,"line":180},[58,7720,678],{"class":64},[58,7722,7723,7725],{"class":60,"line":185},[58,7724,683],{"class":382},[58,7726,686],{"class":99},[58,7728,7729,7731,7733,7735,7737],{"class":60,"line":191},[58,7730,53],{"class":75},[58,7732,693],{"class":99},[58,7734,696],{"class":382},[58,7736,699],{"class":99},[58,7738,702],{"class":382},[58,7740,7741,7743,7745],{"class":60,"line":208},[58,7742,707],{"class":382},[58,7744,710],{"class":99},[58,7746,702],{"class":382},[58,7748,7749],{"class":60,"line":213},[58,7750,717],{"class":99},[11,7752,720],{"id":720},[44,7754,724],{"id":723},[15,7756,727,7757,730,7759,733],{},[19,7758,569],{},[19,7760,569],{},[15,7762,7763,740,7765,744,7767,748,7769,751],{},[737,7764,739],{},[19,7766,743],{},[19,7768,747],{},[19,7770,569],{},[44,7772,755,7773,758,7775,762],{"id":754},[19,7774,34],{},[19,7776,761],{},[15,7778,765,7779,769,7781,772,7783,782],{},[19,7780,768],{},[19,7782,34],{},[737,7784,775,7785,778,7787,781],{},[19,7786,761],{},[19,7788,202],{},[49,7790,7791],{"className":51,"code":785,"language":53,"meta":54,"style":54},[19,7792,7793,7797],{"__ignoreMap":54},[58,7794,7795],{"class":60,"line":61},[58,7796,792],{"class":64},[58,7798,7799],{"class":60,"line":68},[58,7800,188],{"class":64},[15,7802,799,7803,782],{},[19,7804,802],{},[15,7806,7807,807,7809,810],{},[737,7808,739],{},[19,7810,761],{},[49,7812,7813],{"className":813,"code":814,"language":815,"meta":54,"style":54},[19,7814,7815,7819,7825,7831,7837,7843,7877,7881,7885,7889],{"__ignoreMap":54},[58,7816,7817],{"class":60,"line":61},[58,7818,822],{"class":64},[58,7820,7821,7823],{"class":60,"line":68},[58,7822,827],{"class":75},[58,7824,830],{"class":92},[58,7826,7827,7829],{"class":60,"line":79},[58,7828,835],{"class":75},[58,7830,830],{"class":92},[58,7832,7833,7835],{"class":60,"line":86},[58,7834,842],{"class":75},[58,7836,830],{"class":92},[58,7838,7839,7841],{"class":60,"line":96},[58,7840,849],{"class":75},[58,7842,852],{"class":92},[58,7844,7845,7847,7849,7851,7853,7855,7857,7859,7861,7863,7865,7867,7869,7871,7873,7875],{"class":60,"line":109},[58,7846,857],{"class":71},[58,7848,860],{"class":99},[58,7850,863],{"class":92},[58,7852,202],{"class":99},[58,7854,868],{"class":92},[58,7856,202],{"class":99},[58,7858,873],{"class":75},[58,7860,273],{"class":99},[58,7862,878],{"class":99},[58,7864,881],{"class":71},[58,7866,761],{"class":884},[58,7868,878],{"class":99},[58,7870,889],{"class":99},[58,7872,892],{"class":99},[58,7874,895],{"class":99},[58,7876,898],{"class":99},[58,7878,7879],{"class":60,"line":119},[58,7880,903],{"class":92},[58,7882,7883],{"class":60,"line":129},[58,7884,908],{"class":92},[58,7886,7887],{"class":60,"line":139},[58,7888,913],{"class":92},[58,7890,7891],{"class":60,"line":149},[58,7892,918],{"class":92},[15,7894,921,7895,924],{},[19,7896,761],{},[44,7898,928],{"id":927},[15,7900,931,7901,935],{},[19,7902,934],{},[15,7904,7905,940,7907,944],{},[737,7906,739],{},[19,7908,943],{},[11,7910,947],{"id":947},[49,7912,7913],{"className":579,"code":950,"language":581,"meta":54,"style":54},[19,7914,7915,7923,7931],{"__ignoreMap":54},[58,7916,7917,7919,7921],{"class":60,"line":61},[58,7918,957],{"class":75},[58,7920,960],{"class":99},[58,7922,963],{"class":99},[58,7924,7925,7927,7929],{"class":60,"line":68},[58,7926,968],{"class":99},[58,7928,971],{"class":99},[58,7930,974],{"class":99},[58,7932,7933,7935,7937],{"class":60,"line":79},[58,7934,979],{"class":75},[58,7936,982],{"class":99},[58,7938,985],{"class":99},[15,7940,988],{},[15,7942,7943],{},[737,7944,993],{},[995,7946,7947,7957],{},[998,7948,7949],{},[1001,7950,7951,7953,7955],{},[1004,7952,1006],{},[1004,7954,1009],{},[1004,7956,1012],{},[1014,7958,7959,7967],{},[1001,7960,7961,7963,7965],{},[1019,7962,1021],{},[1019,7964,1024],{},[1019,7966,1027],{},[1001,7968,7969,7971,7973],{},[1019,7970,1032],{},[1019,7972,1035],{},[1019,7974,1038],{},[11,7976,1041],{"id":1041},[15,7978,1044,7979,1047],{},[19,7980,34],{},[1049,7982,7983,7989],{},[1052,7984,1054,7985,778,7987,781],{},[19,7986,761],{},[19,7988,202],{},[1052,7990,1061],{},[15,7992,1064],{},[1066,7994,1068],{},{"title":54,"searchDepth":68,"depth":79,"links":7996},[7997,7998,7999,8004,8009,8010],{"id":13,"depth":68,"text":13},{"id":28,"depth":68,"text":28},{"id":42,"depth":68,"text":42,"children":8000},[8001,8002,8003],{"id":46,"depth":79,"text":47},{"id":460,"depth":79,"text":461},{"id":575,"depth":79,"text":576},{"id":720,"depth":68,"text":720,"children":8005},[8006,8007,8008],{"id":723,"depth":79,"text":724},{"id":754,"depth":79,"text":1082},{"id":927,"depth":79,"text":928},{"id":947,"depth":68,"text":947},{"id":1041,"depth":68,"text":1041},{},{"title":5,"description":1086},[1098,1099,1100,1101],{"id":8015,"title":8016,"author":6,"body":8017,"description":9413,"difficulty":2299,"draft":1088,"extension":1089,"image":9414,"meta":9415,"navigation":82,"path":9416,"pinned":1088,"published":9417,"seo":9418,"series":6,"seriesOrder":6,"stem":9419,"tags":9420,"updated":9417,"__hash__":9422},"posts\u002Fposts\u002Fjira-cli-intro\u002Findex.md","jira_cli — 把企业 Jira 装进命令行，AI Agent 直接调用",{"type":8,"value":8018,"toc":9392},[8019,8023,8038,8105,8108,8111,8188,8190,8223,8230,8233,8237,8349,8353,8484,8488,8556,8559,8622,8625,8627,8631,8634,8654,8657,8672,8675,8679,8685,8823,8834,8865,8869,8872,8969,8973,8976,9094,9098,9101,9165,9168,9172,9175,9243,9246,9252,9255,9289,9296,9299,9359,9361,9364,9381,9389],[11,8020,8022],{"id":8021},"jira_cli-是什么","jira_cli 是什么",[15,8024,8025,8030,8031,778,8034,8037],{},[2909,8026,8029],{"href":8027,"rel":8028},"https:\u002F\u002Fgithub.com\u002Fwishesl\u002Fjira_cli",[2913],"jira_cli"," 是一个面向企业 Jira（Server \u002F Data Center）的本地命令行工具，Go 语言编写，MIT 协议开源。它同时提供 ",[737,8032,8033],{},"Cobra CLI",[737,8035,8036],{},"MCP stdio server"," 两种入口，既能当日常命令行用，又能直接接入 AI 编程工具。",[49,8039,8041],{"className":579,"code":8040,"language":581,"meta":54,"style":54},"# 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",[19,8042,8043,8048,8061,8072,8087,8091,8096],{"__ignoreMap":54},[58,8044,8045],{"class":60,"line":61},[58,8046,8047],{"class":64},"# CLI 日常使用\n",[58,8049,8050,8052,8055,8058],{"class":60,"line":68},[58,8051,8029],{"class":75},[58,8053,8054],{"class":99}," issue",[58,8056,8057],{"class":99}," search",[58,8059,8060],{"class":99}," \"project = PROJ ORDER BY updated DESC\"\n",[58,8062,8063,8065,8067,8069],{"class":60,"line":79},[58,8064,8029],{"class":75},[58,8066,8054],{"class":99},[58,8068,4483],{"class":99},[58,8070,8071],{"class":99}," PROJ-123\n",[58,8073,8074,8076,8078,8081,8084],{"class":60,"line":86},[58,8075,8029],{"class":75},[58,8077,8054],{"class":99},[58,8079,8080],{"class":99}," comment",[58,8082,8083],{"class":99}," PROJ-123",[58,8085,8086],{"class":99}," \"处理完成，请测试验证\"\n",[58,8088,8089],{"class":60,"line":96},[58,8090,83],{"emptyLinePlaceholder":82},[58,8092,8093],{"class":60,"line":109},[58,8094,8095],{"class":64},"# MCP 模式 — 给 AI 装上操作 Jira 的手\n",[58,8097,8098,8100,8102],{"class":60,"line":119},[58,8099,8029],{"class":75},[58,8101,4719],{"class":99},[58,8103,8104],{"class":382}," --user=work\n",[11,8106,8107],{"id":8107},"为什么需要它",[15,8109,8110],{},"大多数 Jira 命令行工具只解决\"开发者手动操作 Jira\"这一个场景。但 2026 年 AI 编程工具已经普及，你的 AI Agent（Claude Code、Codex、Cursor 等）也需要读写 Jira。痛点就来了：",[995,8112,8113,8123],{},[998,8114,8115],{},[1001,8116,8117,8120],{},[1004,8118,8119],{},"痛点",[1004,8121,8122],{},"jira_cli 怎么解",[1014,8124,8125,8133,8141,8153,8165,8176],{},[1001,8126,8127,8130],{},[1019,8128,8129],{},"企业 Jira 在内网，没有公网 API",[1019,8131,8132],{},"本地 stdio MCP server，不经过外网",[1001,8134,8135,8138],{},[1019,8136,8137],{},"AI 可能误删、误写 Jira 数据",[1019,8139,8140],{},"账号级只读策略，AI 只能查不能改",[1001,8142,8143,8146],{},[1019,8144,8145],{},"团队多人多 Jira 实例",[1019,8147,8148,8149,8152],{},"多 Profile 管理，",[19,8150,8151],{},"--user=work"," 切换",[1001,8154,8155,8158],{},[1019,8156,8157],{},"Windows 终端体验差",[1019,8159,8160,8161,8164],{},"原生 ",[19,8162,8163],{},".exe"," 构建 + 一键安装脚本",[1001,8166,8167,8170],{},[1019,8168,8169],{},"AI 生成 JQL 困难",[1019,8171,8172,8173],{},"字段元数据发现：",[19,8174,8175],{},"field list --query xxx",[1001,8177,8178,8181],{},[1019,8179,8180],{},"MCP 直接拿原始 JSON 太冗长",[1019,8182,8183,8184,8187],{},"紧凑视图 + ",[19,8185,8186],{},"--view summary"," 减少 token 消耗",[11,8189,6597],{"id":6597},[49,8191,8193],{"className":579,"code":8192,"language":581,"meta":54,"style":54},"# 从源码安装（要求 Go 1.26.3+）\ngo install github.com\u002Fwishesl\u002Fjira_cli@latest\n\n# 或下载预编译二进制\n# https:\u002F\u002Fgithub.com\u002Fwishesl\u002Fjira_cli\u002Freleases\n",[19,8194,8195,8200,8209,8213,8218],{"__ignoreMap":54},[58,8196,8197],{"class":60,"line":61},[58,8198,8199],{"class":64},"# 从源码安装（要求 Go 1.26.3+）\n",[58,8201,8202,8204,8206],{"class":60,"line":68},[58,8203,53],{"class":75},[58,8205,622],{"class":99},[58,8207,8208],{"class":99}," github.com\u002Fwishesl\u002Fjira_cli@latest\n",[58,8210,8211],{"class":60,"line":79},[58,8212,83],{"emptyLinePlaceholder":82},[58,8214,8215],{"class":60,"line":86},[58,8216,8217],{"class":64},"# 或下载预编译二进制\n",[58,8219,8220],{"class":60,"line":96},[58,8221,8222],{"class":64},"# https:\u002F\u002Fgithub.com\u002Fwishesl\u002Fjira_cli\u002Freleases\n",[15,8224,8225,8226,8229],{},"Windows 用户下载 zip 包解压后双击 ",[19,8227,8228],{},"install.bat"," 即可全局安装，自动加入 PATH。",[11,8231,8232],{"id":8232},"快速上手",[44,8234,8236],{"id":8235},"_1-配置认证","1. 配置认证",[49,8238,8240],{"className":579,"code":8239,"language":581,"meta":54,"style":54},"# 设置 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",[19,8241,8242,8247,8263,8267,8272,8292,8296,8301,8330,8334,8339],{"__ignoreMap":54},[58,8243,8244],{"class":60,"line":61},[58,8245,8246],{"class":64},"# 设置 Jira 地址\n",[58,8248,8249,8251,8254,8257,8260],{"class":60,"line":68},[58,8250,8029],{"class":75},[58,8252,8253],{"class":99}," set",[58,8255,8256],{"class":99}," base-url",[58,8258,8259],{"class":99}," https:\u002F\u002Fjira.your-company.com",[58,8261,8262],{"class":382}," --global\n",[58,8264,8265],{"class":60,"line":79},[58,8266,83],{"emptyLinePlaceholder":82},[58,8268,8269],{"class":60,"line":86},[58,8270,8271],{"class":64},"# Token 认证（推荐）\n",[58,8273,8274,8276,8278,8281,8283,8286,8288,8290],{"class":60,"line":96},[58,8275,8029],{"class":75},[58,8277,8253],{"class":99},[58,8279,8280],{"class":99}," token",[58,8282,1630],{"class":71},[58,8284,8285],{"class":99},"your-toke",[58,8287,2032],{"class":92},[58,8289,1639],{"class":71},[58,8291,8262],{"class":382},[58,8293,8294],{"class":60,"line":109},[58,8295,83],{"emptyLinePlaceholder":82},[58,8297,8298],{"class":60,"line":119},[58,8299,8300],{"class":64},"# 或用户名密码认证\n",[58,8302,8303,8305,8307,8309,8311,8314,8316,8318,8320,8323,8326,8328],{"class":60,"line":129},[58,8304,8029],{"class":75},[58,8306,8253],{"class":99},[58,8308,6671],{"class":99},[58,8310,1630],{"class":71},[58,8312,8313],{"class":99},"usernam",[58,8315,2018],{"class":92},[58,8317,1639],{"class":71},[58,8319,1630],{"class":71},[58,8321,8322],{"class":99},"passwor",[58,8324,8325],{"class":92},"d",[58,8327,1639],{"class":71},[58,8329,8262],{"class":382},[58,8331,8332],{"class":60,"line":139},[58,8333,83],{"emptyLinePlaceholder":82},[58,8335,8336],{"class":60,"line":149},[58,8337,8338],{"class":64},"# 验证一下\n",[58,8340,8341,8343,8346],{"class":60,"line":159},[58,8342,8029],{"class":75},[58,8344,8345],{"class":99}," user",[58,8347,8348],{"class":99}," me\n",[44,8350,8352],{"id":8351},"_2-日常使用","2. 日常使用",[49,8354,8356],{"className":579,"code":8355,"language":581,"meta":54,"style":54},"# 查自己今天的任务\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",[19,8357,8358,8363,8374,8378,8383,8393,8397,8402,8416,8433,8437,8442,8455,8459,8464],{"__ignoreMap":54},[58,8359,8360],{"class":60,"line":61},[58,8361,8362],{"class":64},"# 查自己今天的任务\n",[58,8364,8365,8367,8369,8371],{"class":60,"line":68},[58,8366,8029],{"class":75},[58,8368,8054],{"class":99},[58,8370,8057],{"class":99},[58,8372,8373],{"class":99}," \"assignee = currentUser() ORDER BY updated DESC\"\n",[58,8375,8376],{"class":60,"line":79},[58,8377,83],{"emptyLinePlaceholder":82},[58,8379,8380],{"class":60,"line":86},[58,8381,8382],{"class":64},"# 看某个任务详情\n",[58,8384,8385,8387,8389,8391],{"class":60,"line":96},[58,8386,8029],{"class":75},[58,8388,8054],{"class":99},[58,8390,4483],{"class":99},[58,8392,8071],{"class":99},[58,8394,8395],{"class":60,"line":109},[58,8396,83],{"emptyLinePlaceholder":82},[58,8398,8399],{"class":60,"line":119},[58,8400,8401],{"class":64},"# 流转状态\n",[58,8403,8404,8406,8408,8411,8413],{"class":60,"line":129},[58,8405,8029],{"class":75},[58,8407,8054],{"class":99},[58,8409,8410],{"class":99}," transitions",[58,8412,8083],{"class":99},[58,8414,8415],{"class":64},"                    # 可以转哪些\n",[58,8417,8418,8420,8422,8425,8427,8430],{"class":60,"line":139},[58,8419,8029],{"class":75},[58,8421,8054],{"class":99},[58,8423,8424],{"class":99}," do-transition",[58,8426,8083],{"class":99},[58,8428,8429],{"class":382}," 21",[58,8431,8432],{"class":64},"               # 执行流转\n",[58,8434,8435],{"class":60,"line":149},[58,8436,83],{"emptyLinePlaceholder":82},[58,8438,8439],{"class":60,"line":159},[58,8440,8441],{"class":64},"# 添加备注\n",[58,8443,8444,8446,8448,8450,8452],{"class":60,"line":164},[58,8445,8029],{"class":75},[58,8447,8054],{"class":99},[58,8449,8080],{"class":99},[58,8451,8083],{"class":99},[58,8453,8454],{"class":99}," \"已定位到问题，正在修复\"\n",[58,8456,8457],{"class":60,"line":174},[58,8458,83],{"emptyLinePlaceholder":82},[58,8460,8461],{"class":60,"line":180},[58,8462,8463],{"class":64},"# 创建任务\n",[58,8465,8466,8468,8470,8472,8475,8478,8481],{"class":60,"line":185},[58,8467,8029],{"class":75},[58,8469,8054],{"class":99},[58,8471,6557],{"class":99},[58,8473,8474],{"class":99}," PROJ",[58,8476,8477],{"class":99}," Task",[58,8479,8480],{"class":382}," --fields",[58,8482,8483],{"class":99}," @fields.json\n",[44,8485,8487],{"id":8486},"_3-配给-ai-使用","3. 配给 AI 使用",[49,8489,8491],{"className":579,"code":8490,"language":581,"meta":54,"style":54},"# 先配置一个只读账号给 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",[19,8492,8493,8498,8515,8538],{"__ignoreMap":54},[58,8494,8495],{"class":60,"line":61},[58,8496,8497],{"class":64},"# 先配置一个只读账号给 AI 用\n",[58,8499,8500,8502,8504,8506,8508,8510,8513],{"class":60,"line":68},[58,8501,8029],{"class":75},[58,8503,8253],{"class":99},[58,8505,8256],{"class":99},[58,8507,8259],{"class":99},[58,8509,1627],{"class":382},[58,8511,8512],{"class":99}," ai",[58,8514,8262],{"class":382},[58,8516,8517,8519,8521,8523,8525,8528,8530,8532,8534,8536],{"class":60,"line":79},[58,8518,8029],{"class":75},[58,8520,8253],{"class":99},[58,8522,8280],{"class":99},[58,8524,1630],{"class":71},[58,8526,8527],{"class":99},"toke",[58,8529,2032],{"class":92},[58,8531,1639],{"class":71},[58,8533,1627],{"class":382},[58,8535,8512],{"class":99},[58,8537,8262],{"class":382},[58,8539,8540,8542,8544,8547,8550,8552,8554],{"class":60,"line":86},[58,8541,8029],{"class":75},[58,8543,8253],{"class":99},[58,8545,8546],{"class":99}," readonly",[58,8548,8549],{"class":99}," on",[58,8551,1627],{"class":382},[58,8553,8512],{"class":99},[58,8555,8262],{"class":382},[15,8557,8558],{},"然后在 AI 工具的 MCP 配置里加上：",[49,8560,8562],{"className":6269,"code":8561,"language":6271,"meta":54,"style":54},"{\n  \"mcpServers\": {\n    \"jira\": {\n      \"command\": \"jira_cli\",\n      \"args\": [\"mcp\", \"--user=ai\"]\n    }\n  }\n}\n",[19,8563,8564,8568,8574,8581,8593,8610,8614,8618],{"__ignoreMap":54},[58,8565,8566],{"class":60,"line":61},[58,8567,6278],{"class":92},[58,8569,8570,8572],{"class":60,"line":68},[58,8571,6283],{"class":382},[58,8573,830],{"class":92},[58,8575,8576,8579],{"class":60,"line":79},[58,8577,8578],{"class":382},"    \"jira\"",[58,8580,830],{"class":92},[58,8582,8583,8586,8588,8591],{"class":60,"line":86},[58,8584,8585],{"class":382},"      \"command\"",[58,8587,2794],{"class":92},[58,8589,8590],{"class":99},"\"jira_cli\"",[58,8592,2741],{"class":92},[58,8594,8595,8598,8600,8603,8605,8608],{"class":60,"line":96},[58,8596,8597],{"class":382},"      \"args\"",[58,8599,2825],{"class":92},[58,8601,8602],{"class":99},"\"mcp\"",[58,8604,889],{"class":92},[58,8606,8607],{"class":99},"\"--user=ai\"",[58,8609,1552],{"class":92},[58,8611,8612],{"class":60,"line":109},[58,8613,451],{"class":92},[58,8615,8616],{"class":60,"line":119},[58,8617,3050],{"class":92},[58,8619,8620],{"class":60,"line":129},[58,8621,457],{"class":92},[15,8623,8624],{},"这样 AI 就能查 Jira 但不能修改，既有了上下文，又不用担心误操作。",[11,8626,7079],{"id":7079},[44,8628,8630],{"id":8629},"_1-账号级只读策略","1. 账号级只读策略",[15,8632,8633],{},"这是 jira_cli 最实用的设计。给 AI Agent 配置只读账号：",[49,8635,8637],{"className":579,"code":8636,"language":581,"meta":54,"style":54},"jira_cli set readonly on --name ai\n",[19,8638,8639],{"__ignoreMap":54},[58,8640,8641,8643,8645,8647,8649,8651],{"class":60,"line":61},[58,8642,8029],{"class":75},[58,8644,8253],{"class":99},[58,8646,8546],{"class":99},[58,8648,8549],{"class":99},[58,8650,1627],{"class":382},[58,8652,8653],{"class":99}," ai\n",[15,8655,8656],{},"只读模式下：",[2638,8658,8659,8662,8665],{},[1052,8660,8661],{},"CLI 写命令被隐藏和拒绝",[1052,8663,8664],{},"MCP 的写入工具不会注册",[1052,8666,8667,8668,8671],{},"环境变量 ",[19,8669,8670],{},"JIRA_READONLY=true"," 也可以临时覆盖",[15,8673,8674],{},"AI 只能查 JQL、看任务、查字段、读评论，绝对不会误改数据。需要创建\u002F更新时，另配一个有写入权限的账号由开发者自己操作。",[44,8676,8678],{"id":8677},"_2-多-profile-切换","2. 多 Profile 切换",[15,8680,8681,8682,8684],{},"一个配置文件里管理多个 Jira 账号，",[19,8683,1983],{}," 切换：",[49,8686,8688],{"className":579,"code":8687,"language":581,"meta":54,"style":54},"# 工作账号\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",[19,8689,8690,8695,8713,8736,8740,8745,8763,8786,8790,8795,8809],{"__ignoreMap":54},[58,8691,8692],{"class":60,"line":61},[58,8693,8694],{"class":64},"# 工作账号\n",[58,8696,8697,8699,8701,8703,8706,8708,8711],{"class":60,"line":68},[58,8698,8029],{"class":75},[58,8700,8253],{"class":99},[58,8702,8256],{"class":99},[58,8704,8705],{"class":99}," https:\u002F\u002Fjira.company.com",[58,8707,1627],{"class":382},[58,8709,8710],{"class":99}," work",[58,8712,8262],{"class":382},[58,8714,8715,8717,8719,8721,8723,8726,8728,8730,8732,8734],{"class":60,"line":79},[58,8716,8029],{"class":75},[58,8718,8253],{"class":99},[58,8720,8280],{"class":99},[58,8722,1630],{"class":71},[58,8724,8725],{"class":99},"work-toke",[58,8727,2032],{"class":92},[58,8729,1639],{"class":71},[58,8731,1627],{"class":382},[58,8733,8710],{"class":99},[58,8735,8262],{"class":382},[58,8737,8738],{"class":60,"line":86},[58,8739,83],{"emptyLinePlaceholder":82},[58,8741,8742],{"class":60,"line":96},[58,8743,8744],{"class":64},"# 个人账号\n",[58,8746,8747,8749,8751,8753,8756,8758,8761],{"class":60,"line":109},[58,8748,8029],{"class":75},[58,8750,8253],{"class":99},[58,8752,8256],{"class":99},[58,8754,8755],{"class":99}," https:\u002F\u002Fjira.personal.com",[58,8757,1627],{"class":382},[58,8759,8760],{"class":99}," personal",[58,8762,8262],{"class":382},[58,8764,8765,8767,8769,8771,8773,8776,8778,8780,8782,8784],{"class":60,"line":119},[58,8766,8029],{"class":75},[58,8768,8253],{"class":99},[58,8770,8280],{"class":99},[58,8772,1630],{"class":71},[58,8774,8775],{"class":99},"personal-toke",[58,8777,2032],{"class":92},[58,8779,1639],{"class":71},[58,8781,1627],{"class":382},[58,8783,8760],{"class":99},[58,8785,8262],{"class":382},[58,8787,8788],{"class":60,"line":129},[58,8789,83],{"emptyLinePlaceholder":82},[58,8791,8792],{"class":60,"line":139},[58,8793,8794],{"class":64},"# 使用时切换\n",[58,8796,8797,8799,8802,8804,8806],{"class":60,"line":149},[58,8798,8029],{"class":75},[58,8800,8801],{"class":382}," --user=work",[58,8803,8054],{"class":99},[58,8805,8057],{"class":99},[58,8807,8808],{"class":99}," \"project = PROJ\"\n",[58,8810,8811,8813,8816,8818,8820],{"class":60,"line":159},[58,8812,8029],{"class":75},[58,8814,8815],{"class":382}," --user=personal",[58,8817,8054],{"class":99},[58,8819,8057],{"class":99},[58,8821,8822],{"class":99}," \"project = SIDE\"\n",[15,8824,8825,8826,8829,8830,8833],{},"同时支持",[737,8827,8828],{},"项目级配置","（",[19,8831,8832],{},"--local","）和工作目录绑定：",[49,8835,8837],{"className":579,"code":8836,"language":581,"meta":54,"style":54},"# 在项目目录下，配置只对当前项目生效\ncd ~\u002Fprojects\u002Fbackend\njira_cli set base-url https:\u002F\u002Fjira-backend.company.com --local\n",[19,8838,8839,8844,8851],{"__ignoreMap":54},[58,8840,8841],{"class":60,"line":61},[58,8842,8843],{"class":64},"# 在项目目录下，配置只对当前项目生效\n",[58,8845,8846,8848],{"class":60,"line":68},[58,8847,683],{"class":382},[58,8849,8850],{"class":99}," ~\u002Fprojects\u002Fbackend\n",[58,8852,8853,8855,8857,8859,8862],{"class":60,"line":79},[58,8854,8029],{"class":75},[58,8856,8253],{"class":99},[58,8858,8256],{"class":99},[58,8860,8861],{"class":99}," https:\u002F\u002Fjira-backend.company.com",[58,8863,8864],{"class":382}," --local\n",[44,8866,8868],{"id":8867},"_3-对-ai-友好的输出","3. 对 AI 友好的输出",[15,8870,8871],{},"jira_cli 在输出设计上充分考虑了 AI Agent 的 token 消耗：",[49,8873,8875],{"className":579,"code":8874,"language":581,"meta":54,"style":54},"# 紧凑视图 — 去掉 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",[19,8876,8877,8882,8898,8902,8907,8924,8928,8933,8948,8952,8957],{"__ignoreMap":54},[58,8878,8879],{"class":60,"line":61},[58,8880,8881],{"class":64},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\n",[58,8883,8884,8886,8888,8890,8892,8895],{"class":60,"line":68},[58,8885,8029],{"class":75},[58,8887,8054],{"class":99},[58,8889,4483],{"class":99},[58,8891,8083],{"class":99},[58,8893,8894],{"class":382}," --view",[58,8896,8897],{"class":99}," summary\n",[58,8899,8900],{"class":60,"line":79},[58,8901,83],{"emptyLinePlaceholder":82},[58,8903,8904],{"class":60,"line":86},[58,8905,8906],{"class":64},"# 表格输出 — 一眼看到关键信息\n",[58,8908,8909,8911,8913,8915,8918,8921],{"class":60,"line":96},[58,8910,8029],{"class":75},[58,8912,8054],{"class":99},[58,8914,8057],{"class":99},[58,8916,8917],{"class":99}," \"project = PROJ\"",[58,8919,8920],{"class":382}," --output",[58,8922,8923],{"class":99}," table\n",[58,8925,8926],{"class":60,"line":109},[58,8927,83],{"emptyLinePlaceholder":82},[58,8929,8930],{"class":60,"line":119},[58,8931,8932],{"class":64},"# JSON 输出 — 程序解析，不转义 Unicode\n",[58,8934,8935,8937,8939,8942,8944,8946],{"class":60,"line":129},[58,8936,8029],{"class":75},[58,8938,8920],{"class":382},[58,8940,8941],{"class":99}," json",[58,8943,8054],{"class":99},[58,8945,4483],{"class":99},[58,8947,8071],{"class":99},[58,8949,8950],{"class":60,"line":139},[58,8951,83],{"emptyLinePlaceholder":82},[58,8953,8954],{"class":60,"line":149},[58,8955,8956],{"class":64},"# 自定义搜索列 — 只要需要的字段\n",[58,8958,8959,8961,8963,8966],{"class":60,"line":159},[58,8960,8029],{"class":75},[58,8962,8253],{"class":99},[58,8964,8965],{"class":99}," search-columns",[58,8967,8968],{"class":99}," key,summary,fixVersions\n",[44,8970,8972],{"id":8971},"_4-字段元数据发现","4. 字段元数据发现",[15,8974,8975],{},"Jira 的 customfield 让人头疼，jira_cli 提供了完整的字段发现能力：",[49,8977,8979],{"className":579,"code":8978,"language":581,"meta":54,"style":54},"# 列出所有字段及其类型\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",[19,8980,8981,8986,8995,8999,9004,9018,9022,9027,9038,9042,9047,9064,9068,9073],{"__ignoreMap":54},[58,8982,8983],{"class":60,"line":61},[58,8984,8985],{"class":64},"# 列出所有字段及其类型\n",[58,8987,8988,8990,8993],{"class":60,"line":68},[58,8989,8029],{"class":75},[58,8991,8992],{"class":99}," field",[58,8994,3137],{"class":99},[58,8996,8997],{"class":60,"line":79},[58,8998,83],{"emptyLinePlaceholder":82},[58,9000,9001],{"class":60,"line":86},[58,9002,9003],{"class":64},"# 搜索包含特定关键词的字段\n",[58,9005,9006,9008,9010,9012,9015],{"class":60,"line":96},[58,9007,8029],{"class":75},[58,9009,8992],{"class":99},[58,9011,6492],{"class":99},[58,9013,9014],{"class":382}," --query",[58,9016,9017],{"class":99}," \"版本\"\n",[58,9019,9020],{"class":60,"line":109},[58,9021,83],{"emptyLinePlaceholder":82},[58,9023,9024],{"class":60,"line":119},[58,9025,9026],{"class":64},"# 只看自定义字段\n",[58,9028,9029,9031,9033,9035],{"class":60,"line":129},[58,9030,8029],{"class":75},[58,9032,8992],{"class":99},[58,9034,6492],{"class":99},[58,9036,9037],{"class":382}," --custom-only\n",[58,9039,9040],{"class":60,"line":139},[58,9041,83],{"emptyLinePlaceholder":82},[58,9043,9044],{"class":60,"line":149},[58,9045,9046],{"class":64},"# 创建任务前，先看看需要哪些字段\n",[58,9048,9049,9051,9053,9056,9058,9061],{"class":60,"line":159},[58,9050,8029],{"class":75},[58,9052,8054],{"class":99},[58,9054,9055],{"class":99}," create-meta",[58,9057,8474],{"class":99},[58,9059,9060],{"class":382}," --issue-type",[58,9062,9063],{"class":99}," Task\n",[58,9065,9066],{"class":60,"line":164},[58,9067,83],{"emptyLinePlaceholder":82},[58,9069,9070],{"class":60,"line":174},[58,9071,9072],{"class":64},"# 分页查看可选值\n",[58,9074,9075,9077,9079,9082,9084,9086,9089,9091],{"class":60,"line":180},[58,9076,8029],{"class":75},[58,9078,8054],{"class":99},[58,9080,9081],{"class":99}," create-meta-values",[58,9083,8474],{"class":99},[58,9085,8477],{"class":99},[58,9087,9088],{"class":99}," priority",[58,9090,9014],{"class":382},[58,9092,9093],{"class":99}," High\n",[44,9095,9097],{"id":9096},"_5-mcp-写入控制","5. MCP 写入控制",[15,9099,9100],{},"除了全局只读，MCP 还有更细粒度的控制：",[995,9102,9103,9113],{},[998,9104,9105],{},[1001,9106,9107,9110],{},[1004,9108,9109],{},"环境变量",[1004,9111,9112],{},"作用",[1014,9114,9115,9125,9135,9145,9155],{},[1001,9116,9117,9122],{},[1019,9118,9119],{},[19,9120,9121],{},"JIRA_CREATE_OPEN=false",[1019,9123,9124],{},"禁用创建\u002F更新工具",[1001,9126,9127,9132],{},[1019,9128,9129],{},[19,9130,9131],{},"JIRA_DISABLE_DISCLAIMER=true",[1019,9133,9134],{},"去掉 AI 自动添加的备注声明",[1001,9136,9137,9142],{},[1019,9138,9139],{},[19,9140,9141],{},"JIRA_CREATE_SUMMARY_MAX",[1019,9143,9144],{},"限制摘要长度",[1001,9146,9147,9152],{},[1019,9148,9149],{},[19,9150,9151],{},"JIRA_CREATE_DESCRIPTION_MAX",[1019,9153,9154],{},"限制描述长度",[1001,9156,9157,9162],{},[1019,9158,9159],{},[19,9160,9161],{},"JIRA_CREATE_FIELDS_MAX_BYTES",[1019,9163,9164],{},"限制创建字段的 JSON 大小",[15,9166,9167],{},"这些限制确保 AI 即使有写入权限也不会\"失控\"——例如防止 AI 往描述里塞几万字的废话。",[44,9169,9171],{"id":9170},"_6-中英双语帮助","6. 中英双语帮助",[15,9173,9174],{},"帮助信息默认中文，也支持一键切英文：",[49,9176,9178],{"className":579,"code":9177,"language":581,"meta":54,"style":54},"# 保存英文偏好\njira_cli set help-language en\n\n# 切回中文\njira_cli set help-language zh\n\n# 临时覆盖\njira_cli --lang en issue search -h\n",[19,9179,9180,9185,9197,9201,9206,9217,9221,9226],{"__ignoreMap":54},[58,9181,9182],{"class":60,"line":61},[58,9183,9184],{"class":64},"# 保存英文偏好\n",[58,9186,9187,9189,9191,9194],{"class":60,"line":68},[58,9188,8029],{"class":75},[58,9190,8253],{"class":99},[58,9192,9193],{"class":99}," help-language",[58,9195,9196],{"class":99}," en\n",[58,9198,9199],{"class":60,"line":79},[58,9200,83],{"emptyLinePlaceholder":82},[58,9202,9203],{"class":60,"line":86},[58,9204,9205],{"class":64},"# 切回中文\n",[58,9207,9208,9210,9212,9214],{"class":60,"line":96},[58,9209,8029],{"class":75},[58,9211,8253],{"class":99},[58,9213,9193],{"class":99},[58,9215,9216],{"class":99}," zh\n",[58,9218,9219],{"class":60,"line":109},[58,9220,83],{"emptyLinePlaceholder":82},[58,9222,9223],{"class":60,"line":119},[58,9224,9225],{"class":64},"# 临时覆盖\n",[58,9227,9228,9230,9233,9236,9238,9240],{"class":60,"line":129},[58,9229,8029],{"class":75},[58,9231,9232],{"class":382}," --lang",[58,9234,9235],{"class":99}," en",[58,9237,8054],{"class":99},[58,9239,8057],{"class":99},[58,9241,9242],{"class":382}," -h\n",[11,9244,9245],{"id":9245},"架构一览",[49,9247,9250],{"className":9248,"code":9249,"language":2407},[2405],"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",[19,9251,9249],{"__ignoreMap":54},[15,9253,9254],{},"设计原则清晰：",[2638,9256,9257,9265,9273,9281],{},[1052,9258,9259,9264],{},[737,9260,9261],{},[19,9262,9263],{},"pkg\u002Fjira"," 是纯 Jira 客户端，HTTP、认证、上下文处理都在这里",[1052,9266,9267,9272],{},[737,9268,9269],{},[19,9270,9271],{},"cmd"," 只管 Cobra 解析和呈现",[1052,9274,9275,9280],{},[737,9276,9277],{},[19,9278,9279],{},"internal\u002Ftools\u002Fjira_tools"," 只管 MCP schema 验证和结果适配",[1052,9282,9283,9288],{},[737,9284,9285],{},[19,9286,9287],{},"internal\u002Fconfig"," 拥有 Profile 的全部逻辑，不分散到 cmd",[15,9290,9291,9292,9295],{},"每一层职责单一，新增 Jira 能力时按 ",[19,9293,9294],{},"endpoints.go → Client 方法 → CLI 命令 → MCP tool → 策略注册"," 的路径走就行。",[11,9297,9298],{"id":9298},"和已有方案的对比",[995,9300,9301,9313],{},[998,9302,9303],{},[1001,9304,9305,9307,9310],{},[1004,9306,3752],{},[1004,9308,9309],{},"适合场景",[1004,9311,9312],{},"jira_cli 的区别",[1014,9314,9315,9326,9337,9348],{},[1001,9316,9317,9320,9323],{},[1019,9318,9319],{},"Atlassian Rovo MCP Server",[1019,9321,9322],{},"Jira Cloud、官方 OAuth",[1019,9324,9325],{},"jira_cli 是本地 stdio，适合内网 Server\u002FDC",[1001,9327,9328,9331,9334],{},[1019,9329,9330],{},"go-jira \u002F jira-cli 社区版",[1019,9332,9333],{},"纯命令行管理",[1019,9335,9336],{},"jira_cli 同时提供 MCP server，面向 AI 工作流",[1001,9338,9339,9342,9345],{},[1019,9340,9341],{},"直接调 REST API",[1019,9343,9344],{},"自定义集成",[1019,9346,9347],{},"jira_cli 封装了认证、只读策略、字段元数据",[1001,9349,9350,9353,9356],{},[1019,9351,9352],{},"Jira 网页",[1019,9354,9355],{},"日常使用",[1019,9357,9358],{},"jira_cli 把 Jira 变成终端里的一行命令",[11,9360,1041],{"id":1041},[15,9362,9363],{},"如果你的团队满足以下任一条件，jira_cli 值得一试：",[2638,9365,9366,9369,9372,9375,9378],{},[1052,9367,9368],{},"企业 Jira 部署在内网，想接入 AI 编程工具",[1052,9370,9371],{},"想给 AI Agent 只读权限来查 Jira 上下文",[1052,9373,9374],{},"需要命令行快速查看\u002F操作 Jira，不想切浏览器",[1052,9376,9377],{},"需要管理多个 Jira 实例和多套账号",[1052,9379,9380],{},"Windows 用户想要无摩擦的 CLI 体验",[15,9382,9383,9384,9388],{},"GitHub：",[2909,9385,9387],{"href":8027,"rel":9386},[2913],"github.com\u002Fwishesl\u002Fjira_cli"," | MIT 协议 | Go 1.26.3+",[1066,9390,9391],{},"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":54,"searchDepth":68,"depth":79,"links":9393},[9394,9395,9396,9397,9402,9410,9411,9412],{"id":8021,"depth":68,"text":8022},{"id":8107,"depth":68,"text":8107},{"id":6597,"depth":68,"text":6597},{"id":8232,"depth":68,"text":8232,"children":9398},[9399,9400,9401],{"id":8235,"depth":79,"text":8236},{"id":8351,"depth":79,"text":8352},{"id":8486,"depth":79,"text":8487},{"id":7079,"depth":68,"text":7079,"children":9403},[9404,9405,9406,9407,9408,9409],{"id":8629,"depth":79,"text":8630},{"id":8677,"depth":79,"text":8678},{"id":8867,"depth":79,"text":8868},{"id":8971,"depth":79,"text":8972},{"id":9096,"depth":79,"text":9097},{"id":9170,"depth":79,"text":9171},{"id":9245,"depth":68,"text":9245},{"id":9298,"depth":68,"text":9298},{"id":1041,"depth":68,"text":1041},"介绍 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":8016,"description":9413},"posts\u002Fjira-cli-intro\u002Findex",[9421,7229,6444,1098,6446,7230],"Jira","V1-ms-ndMO09C96sfR-xfjgfdErA4lWoy1XiFen6Izw",1784303541089]