[{"data":1,"prerenderedAt":7224},["ShallowReactive",2],{"posts-list":3},[4,463,1521,2307,4359,4651,5814,6032,6456],{"id":5,"title":6,"author":7,"body":8,"description":450,"draft":451,"extension":452,"image":453,"meta":454,"navigation":394,"path":455,"pinned":394,"published":456,"seo":457,"stem":458,"tags":459,"__hash__":462},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎来到尼克的小窝",null,{"type":9,"value":10,"toc":441},"minimark",[11,16,25,28,31,34,114,117,120,184,187,190,277,280,349,352,355,373,380,383,415,418,429,432,437],[12,13,15],"h2",{"id":14},"博客上线啦","博客上线啦！",[17,18,19,20,24],"p",{},"Hello World！欢迎来到",[21,22,23],"strong",{},"尼克的小窝","，这是我的第一篇博客文章。",[17,26,27],{},"经过一段时间的折腾，这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。",[12,29,30],{"id":30},"技术栈",[17,32,33],{},"本站使用了以下技术：",[35,36,37,50],"table",{},[38,39,40],"thead",{},[41,42,43,47],"tr",{},[44,45,46],"th",{},"技术",[44,48,49],{},"用途",[51,52,53,64,74,84,94,104],"tbody",{},[41,54,55,61],{},[56,57,58],"td",{},[21,59,60],{},"Nuxt 3",[56,62,63],{},"Vue 3 全栈框架，提供 SSG、路由、API 等能力",[41,65,66,71],{},[56,67,68],{},[21,69,70],{},"TypeScript",[56,72,73],{},"类型安全，提升开发体验",[41,75,76,81],{},[56,77,78],{},[21,79,80],{},"Tailwind CSS v4",[56,82,83],{},"原子化 CSS，快速构建 UI",[41,85,86,91],{},[56,87,88],{},[21,89,90],{},"@nuxt\u002Fcontent",[56,92,93],{},"Markdown 驱动的内容管理",[41,95,96,101],{},[56,97,98],{},[21,99,100],{},"Shiki",[56,102,103],{},"代码语法高亮",[41,105,106,111],{},[56,107,108],{},[21,109,110],{},"Giscus",[56,112,113],{},"基于 GitHub Discussions 的评论系统",[12,115,116],{"id":116},"功能特性",[17,118,119],{},"目前已实现的功能：",[121,122,123,130,136,142,148,154,160,166,172,178],"ul",{},[124,125,126,129],"li",{},[21,127,128],{},"博客系统"," - Markdown 文章，支持代码高亮、目录导航、搜索",[124,131,132,135],{},[21,133,134],{},"暗色模式"," - 跟随系统或手动切换",[124,137,138,141],{},[21,139,140],{},"文章搜索"," - 支持标题、描述、正文、标签多维度搜索",[124,143,144,147],{},[21,145,146],{},"标签系统"," - 文章标签分类，高频标签快速筛选",[124,149,150,153],{},[21,151,152],{},"友链页面"," - 展示友情链接，支持申请",[124,155,156,159],{},[21,157,158],{},"赞助页面"," - 微信收款码展示",[124,161,162,165],{},[21,163,164],{},"封面制作"," - 在线生成封面图片工具",[124,167,168,171],{},[21,169,170],{},"网易云热评"," - 首页随机展示音乐评论",[124,173,174,177],{},[21,175,176],{},"访客统计"," - 不蒜子计数器，带数字滚动动画",[124,179,180,183],{},[21,181,182],{},"SEO 优化"," - 自动生成站点地图、Open Graph 标签",[12,185,186],{"id":186},"代码示例",[17,188,189],{},"本站的配置非常集中，一个文件搞定全站定制：",[191,192,197],"pre",{"className":193,"code":194,"language":195,"meta":196,"style":196},"language-typescript shiki shiki-themes github-light github-dark","\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","typescript","",[198,199,200,209,230,243,254,265,271],"code",{"__ignoreMap":196},[201,202,205],"span",{"class":203,"line":204},"line",1,[201,206,208],{"class":207},"sHbNN","\u002F\u002F config\u002Fsite.ts\n",[201,210,212,216,219,223,226],{"class":203,"line":211},2,[201,213,215],{"class":214},"siTax","export",[201,217,218],{"class":214}," const",[201,220,222],{"class":221},"suQ91"," siteConfig",[201,224,225],{"class":214}," =",[201,227,229],{"class":228},"sIX_F"," {\n",[201,231,233,236,240],{"class":203,"line":232},3,[201,234,235],{"class":228},"  siteName: ",[201,237,239],{"class":238},"scXbn","'尼克的小窝'",[201,241,242],{"class":228},",\n",[201,244,246,249,252],{"class":203,"line":245},4,[201,247,248],{"class":228},"  url: ",[201,250,251],{"class":238},"'https:\u002F\u002Fnixus.top\u002F'",[201,253,242],{"class":228},[201,255,257,260,263],{"class":203,"line":256},5,[201,258,259],{"class":228},"  avatar: ",[201,261,262],{"class":238},"'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100'",[201,264,242],{"class":228},[201,266,268],{"class":203,"line":267},6,[201,269,270],{"class":207},"  \u002F\u002F ... 改这一个文件，全站生效\n",[201,272,274],{"class":203,"line":273},7,[201,275,276],{"class":228},"}\n",[17,278,279],{},"文章的 frontmatter 也很简洁：",[191,281,285],{"className":282,"code":283,"language":284,"meta":196,"style":196},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 文章标题\npublished: 2026-05-31T08:00:00\ndescription: 文章简介\ntags: ['Nuxt', 'TypeScript']\n---\n","yaml",[198,286,287,293,305,315,325,345],{"__ignoreMap":196},[201,288,289],{"class":203,"line":204},[201,290,292],{"class":291},"sw2iP","---\n",[201,294,295,299,302],{"class":203,"line":211},[201,296,298],{"class":297},"sbB4o","title",[201,300,301],{"class":228},": ",[201,303,304],{"class":238},"文章标题\n",[201,306,307,310,312],{"class":203,"line":232},[201,308,309],{"class":297},"published",[201,311,301],{"class":228},[201,313,314],{"class":221},"2026-05-31T08:00:00\n",[201,316,317,320,322],{"class":203,"line":245},[201,318,319],{"class":297},"description",[201,321,301],{"class":228},[201,323,324],{"class":238},"文章简介\n",[201,326,327,330,333,336,339,342],{"class":203,"line":256},[201,328,329],{"class":297},"tags",[201,331,332],{"class":228},": [",[201,334,335],{"class":238},"'Nuxt'",[201,337,338],{"class":228},", ",[201,340,341],{"class":238},"'TypeScript'",[201,343,344],{"class":228},"]\n",[201,346,347],{"class":203,"line":267},[201,348,292],{"class":291},[12,350,351],{"id":351},"部署",[17,353,354],{},"本站使用 SSG 静态生成，构建后直接部署到静态托管即可：",[191,356,360],{"className":357,"code":358,"language":359,"meta":196,"style":196},"language-bash shiki shiki-themes github-light github-dark","npx nuxi generate\n","bash",[198,361,362],{"__ignoreMap":196},[201,363,364,367,370],{"class":203,"line":204},[201,365,366],{"class":291},"npx",[201,368,369],{"class":238}," nuxi",[201,371,372],{"class":238}," generate\n",[17,374,375,376,379],{},"构建产物在 ",[198,377,378],{},".output\u002Fpublic"," 目录下，可以直接丢到 Nginx、Vercel、Netlify 等平台。",[12,381,382],{"id":382},"后续计划",[121,384,387,397,403,409],{"className":385},[386],"contains-task-list",[124,388,391,396],{"className":389},[390],"task-list-item",[392,393],"input",{"disabled":394,"type":395},true,"checkbox"," RSS 订阅",[124,398,400,402],{"className":399},[390],[392,401],{"disabled":394,"type":395}," 文章目录优化",[124,404,406,408],{"className":405},[390],[392,407],{"disabled":394,"type":395}," 更多小工具",[124,410,412,414],{"className":411},[390],[392,413],{"disabled":394,"type":395}," 持续输出内容",[12,416,417],{"id":417},"致谢",[17,419,420,421,428],{},"感谢 ",[422,423,427],"a",{"href":424,"rel":425},"https:\u002F\u002F2x.nz",[426],"nofollow","二叉树树"," 的开源项目，本站的 UI 设计参考了他的项目。",[430,431],"hr",{},[17,433,434],{},[21,435,436],{},"感谢阅读！如果觉得不错，欢迎点个 Star 或者留个评论 (☞ﾟヮﾟ)☞",[438,439,440],"style",{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .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 .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":196,"searchDepth":211,"depth":232,"links":442},[443,444,445,446,447,448,449],{"id":14,"depth":211,"text":15},{"id":30,"depth":211,"text":30},{"id":116,"depth":211,"text":116},{"id":186,"depth":211,"text":186},{"id":351,"depth":211,"text":351},{"id":382,"depth":211,"text":382},{"id":417,"depth":211,"text":417},"博客上线啦！这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。",false,"md","\u002Fposts\u002Fhello-world\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fhello-world","2026-05-31T08:00:00",{"title":6,"description":450},"posts\u002Fhello-world\u002Findex",[460,461],"博客","Nuxt","jEvVEwWZ3acvWRfFhQcUr8kOuYhMnQ2wbyICRKhZAhw",{"id":464,"title":465,"author":7,"body":466,"description":1508,"draft":451,"extension":452,"image":1509,"meta":1510,"navigation":394,"path":1511,"pinned":451,"published":1512,"seo":1513,"stem":1514,"tags":1515,"__hash__":1520},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",{"type":9,"value":467,"toc":1491},[468,471,478,481,484,495,498,503,893,897,1008,1012,1150,1153,1157,1166,1183,1194,1214,1228,1234,1242,1349,1355,1359,1366,1375,1378,1416,1419,1424,1463,1466,1472,1485,1488],[12,469,470],{"id":470},"背景",[17,472,473,474,477],{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[198,475,476],{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[17,479,480],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[12,482,483],{"id":483},"核心思路",[17,485,486,487,490,491,494],{},"利用 Go 1.16 引入的 ",[198,488,489],{},"\u002F\u002Fgo:embed"," 指令，在编译时将前端 ",[198,492,493],{},"dist\u002F"," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[12,496,497],{"id":497},"逐步实现",[499,500,502],"h3",{"id":501},"_1-创建嵌入模块","1. 创建嵌入模块",[191,504,508],{"className":505,"code":506,"language":507,"meta":196,"style":196},"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",[198,509,510,515,523,528,536,547,556,565,575,585,595,600,610,616,621,627,644,649,671,694,699,728,750,767,779,785,790,806,822,828,848,853,858,882,888],{"__ignoreMap":196},[201,511,512],{"class":203,"line":204},[201,513,514],{"class":207},"\u002F\u002F service\u002Fweb\u002Fembed.go\n",[201,516,517,520],{"class":203,"line":211},[201,518,519],{"class":214},"package",[201,521,522],{"class":291}," web\n",[201,524,525],{"class":203,"line":232},[201,526,527],{"emptyLinePlaceholder":394},"\n",[201,529,530,533],{"class":203,"line":245},[201,531,532],{"class":214},"import",[201,534,535],{"class":228}," (\n",[201,537,538,541,544],{"class":203,"line":256},[201,539,540],{"class":238},"    \"",[201,542,543],{"class":291},"embed",[201,545,546],{"class":238},"\"\n",[201,548,549,551,554],{"class":203,"line":267},[201,550,540],{"class":238},[201,552,553],{"class":291},"io\u002Ffs",[201,555,546],{"class":238},[201,557,558,560,563],{"class":203,"line":273},[201,559,540],{"class":238},[201,561,562],{"class":291},"mime",[201,564,546],{"class":238},[201,566,568,570,573],{"class":203,"line":567},8,[201,569,540],{"class":238},[201,571,572],{"class":291},"net\u002Fhttp",[201,574,546],{"class":238},[201,576,578,580,583],{"class":203,"line":577},9,[201,579,540],{"class":238},[201,581,582],{"class":291},"path\u002Ffilepath",[201,584,546],{"class":238},[201,586,588,590,593],{"class":203,"line":587},10,[201,589,540],{"class":238},[201,591,592],{"class":291},"strings",[201,594,546],{"class":238},[201,596,598],{"class":203,"line":597},11,[201,599,527],{"emptyLinePlaceholder":394},[201,601,603,605,608],{"class":203,"line":602},12,[201,604,540],{"class":238},[201,606,607],{"class":291},"github.com\u002Fgin-gonic\u002Fgin",[201,609,546],{"class":238},[201,611,613],{"class":203,"line":612},13,[201,614,615],{"class":228},")\n",[201,617,619],{"class":203,"line":618},14,[201,620,527],{"emptyLinePlaceholder":394},[201,622,624],{"class":203,"line":623},15,[201,625,626],{"class":207},"\u002F\u002Fgo:embed dist\n",[201,628,630,633,636,638,641],{"class":203,"line":629},16,[201,631,632],{"class":214},"var",[201,634,635],{"class":228}," embedFS ",[201,637,543],{"class":291},[201,639,640],{"class":228},".",[201,642,643],{"class":291},"FS\n",[201,645,647],{"class":203,"line":646},17,[201,648,527],{"emptyLinePlaceholder":394},[201,650,652,655,658,661,664,666,669],{"class":203,"line":651},18,[201,653,654],{"class":214},"func",[201,656,657],{"class":291}," SPAHandler",[201,659,660],{"class":228},"() ",[201,662,663],{"class":291},"gin",[201,665,640],{"class":228},[201,667,668],{"class":291},"HandlerFunc",[201,670,229],{"class":228},[201,672,674,677,680,683,686,689,692],{"class":203,"line":673},19,[201,675,676],{"class":228},"    sub, _ ",[201,678,679],{"class":214},":=",[201,681,682],{"class":228}," fs.",[201,684,685],{"class":291},"Sub",[201,687,688],{"class":228},"(embedFS, ",[201,690,691],{"class":238},"\"dist\"",[201,693,615],{"class":228},[201,695,697],{"class":203,"line":696},20,[201,698,527],{"emptyLinePlaceholder":394},[201,700,702,705,708,711,715,718,720,722,725],{"class":203,"line":701},21,[201,703,704],{"class":214},"    return",[201,706,707],{"class":214}," func",[201,709,710],{"class":228},"(",[201,712,714],{"class":713},"sPK6S","c",[201,716,717],{"class":214}," *",[201,719,663],{"class":291},[201,721,640],{"class":228},[201,723,724],{"class":291},"Context",[201,726,727],{"class":228},") {\n",[201,729,731,734,736,739,742,745,748],{"class":203,"line":730},22,[201,732,733],{"class":228},"        p ",[201,735,679],{"class":214},[201,737,738],{"class":228}," strings.",[201,740,741],{"class":291},"TrimPrefix",[201,743,744],{"class":228},"(c.Request.URL.Path, ",[201,746,747],{"class":238},"\"\u002F\"",[201,749,615],{"class":228},[201,751,753,756,759,762,765],{"class":203,"line":752},23,[201,754,755],{"class":214},"        if",[201,757,758],{"class":228}," p ",[201,760,761],{"class":214},"==",[201,763,764],{"class":238}," \"\"",[201,766,229],{"class":228},[201,768,770,773,776],{"class":203,"line":769},24,[201,771,772],{"class":228},"            p ",[201,774,775],{"class":214},"=",[201,777,778],{"class":238}," \"index.html\"\n",[201,780,782],{"class":203,"line":781},25,[201,783,784],{"class":228},"        }\n",[201,786,788],{"class":203,"line":787},26,[201,789,527],{"emptyLinePlaceholder":394},[201,791,793,796,798,800,803],{"class":203,"line":792},27,[201,794,795],{"class":228},"        data, err ",[201,797,679],{"class":214},[201,799,682],{"class":228},[201,801,802],{"class":291},"ReadFile",[201,804,805],{"class":228},"(sub, p)\n",[201,807,809,811,814,817,820],{"class":203,"line":808},28,[201,810,755],{"class":214},[201,812,813],{"class":228}," err ",[201,815,816],{"class":214},"!=",[201,818,819],{"class":221}," nil",[201,821,229],{"class":228},[201,823,825],{"class":203,"line":824},29,[201,826,827],{"class":207},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[201,829,831,834,836,838,840,843,846],{"class":203,"line":830},30,[201,832,833],{"class":228},"            data, _ ",[201,835,775],{"class":214},[201,837,682],{"class":228},[201,839,802],{"class":291},[201,841,842],{"class":228},"(sub, ",[201,844,845],{"class":238},"\"index.html\"",[201,847,615],{"class":228},[201,849,851],{"class":203,"line":850},31,[201,852,784],{"class":228},[201,854,856],{"class":203,"line":855},32,[201,857,527],{"emptyLinePlaceholder":394},[201,859,861,864,867,870,873,876,879],{"class":203,"line":860},33,[201,862,863],{"class":228},"        c.",[201,865,866],{"class":291},"Data",[201,868,869],{"class":228},"(http.StatusOK, mime.",[201,871,872],{"class":291},"TypeByExtension",[201,874,875],{"class":228},"(filepath.",[201,877,878],{"class":291},"Ext",[201,880,881],{"class":228},"(p)), data)\n",[201,883,885],{"class":203,"line":884},34,[201,886,887],{"class":228},"    }\n",[201,889,891],{"class":203,"line":890},35,[201,892,276],{"class":228},[499,894,896],{"id":895},"_2-改造路由","2. 改造路由",[191,898,900],{"className":505,"code":899,"language":507,"meta":196,"style":196},"\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",[198,901,902,907,919,923,928,944,959,972,985,998],{"__ignoreMap":196},[201,903,904],{"class":203,"line":204},[201,905,906],{"class":207},"\u002F\u002F router.go\n",[201,908,909,911,914,917],{"class":203,"line":211},[201,910,532],{"class":214},[201,912,913],{"class":238}," \"",[201,915,916],{"class":291},"myproject\u002Fweb",[201,918,546],{"class":238},[201,920,921],{"class":203,"line":232},[201,922,527],{"emptyLinePlaceholder":394},[201,924,925],{"class":203,"line":245},[201,926,927],{"class":207},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[201,929,930,933,935,938,941],{"class":203,"line":256},[201,931,932],{"class":228},"webHandler ",[201,934,679],{"class":214},[201,936,937],{"class":228}," web.",[201,939,940],{"class":291},"SPAHandler",[201,942,943],{"class":228},"()\n",[201,945,946,949,952,954,956],{"class":203,"line":267},[201,947,948],{"class":228},"router.",[201,950,951],{"class":291},"GET",[201,953,710],{"class":228},[201,955,747],{"class":238},[201,957,958],{"class":228},", webHandler)\n",[201,960,961,963,965,967,970],{"class":203,"line":273},[201,962,948],{"class":228},[201,964,951],{"class":291},[201,966,710],{"class":228},[201,968,969],{"class":238},"\"\u002Fassets\u002F*filepath\"",[201,971,958],{"class":228},[201,973,974,976,978,980,983],{"class":203,"line":567},[201,975,948],{"class":228},[201,977,951],{"class":291},[201,979,710],{"class":228},[201,981,982],{"class":238},"\"\u002Ffavicon.ico\"",[201,984,958],{"class":228},[201,986,987,989,991,993,996],{"class":203,"line":577},[201,988,948],{"class":228},[201,990,951],{"class":291},[201,992,710],{"class":228},[201,994,995],{"class":238},"\"\u002Ffavicon.svg\"",[201,997,958],{"class":228},[201,999,1000,1002,1005],{"class":203,"line":587},[201,1001,948],{"class":228},[201,1003,1004],{"class":291},"NoRoute",[201,1006,1007],{"class":228},"(webHandler)\n",[499,1009,1011],{"id":1010},"_3-一键构建脚本","3. 一键构建脚本",[191,1013,1015],{"className":357,"code":1014,"language":359,"meta":196,"style":196},"#!\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",[198,1016,1017,1022,1027,1031,1039,1043,1048,1059,1069,1073,1078,1089,1102,1106,1111,1119,1135,1145],{"__ignoreMap":196},[201,1018,1019],{"class":203,"line":204},[201,1020,1021],{"class":207},"#!\u002Fbin\u002Fbash\n",[201,1023,1024],{"class":203,"line":211},[201,1025,1026],{"class":207},"# build-standalone.sh\n",[201,1028,1029],{"class":203,"line":232},[201,1030,527],{"emptyLinePlaceholder":394},[201,1032,1033,1036],{"class":203,"line":245},[201,1034,1035],{"class":221},"set",[201,1037,1038],{"class":221}," -e\n",[201,1040,1041],{"class":203,"line":256},[201,1042,527],{"emptyLinePlaceholder":394},[201,1044,1045],{"class":203,"line":267},[201,1046,1047],{"class":207},"# 1. 构建前端\n",[201,1049,1050,1053,1056],{"class":203,"line":273},[201,1051,1052],{"class":291},"pnpm",[201,1054,1055],{"class":238}," install",[201,1057,1058],{"class":221}," --no-frozen-lockfile\n",[201,1060,1061,1063,1066],{"class":203,"line":567},[201,1062,366],{"class":291},[201,1064,1065],{"class":238}," vite",[201,1067,1068],{"class":238}," build\n",[201,1070,1071],{"class":203,"line":577},[201,1072,527],{"emptyLinePlaceholder":394},[201,1074,1075],{"class":203,"line":587},[201,1076,1077],{"class":207},"# 2. 复制到嵌入目录\n",[201,1079,1080,1083,1086],{"class":203,"line":597},[201,1081,1082],{"class":291},"rm",[201,1084,1085],{"class":221}," -rf",[201,1087,1088],{"class":238}," service\u002Fweb\u002Fdist\n",[201,1090,1091,1094,1097,1100],{"class":203,"line":602},[201,1092,1093],{"class":291},"cp",[201,1095,1096],{"class":221}," -r",[201,1098,1099],{"class":238}," dist",[201,1101,1088],{"class":238},[201,1103,1104],{"class":203,"line":612},[201,1105,527],{"emptyLinePlaceholder":394},[201,1107,1108],{"class":203,"line":618},[201,1109,1110],{"class":207},"# 3. 编译 Go 二进制\n",[201,1112,1113,1116],{"class":203,"line":623},[201,1114,1115],{"class":221},"cd",[201,1117,1118],{"class":238}," service\n",[201,1120,1121,1123,1126,1129,1132],{"class":203,"line":629},[201,1122,507],{"class":291},[201,1124,1125],{"class":238}," build",[201,1127,1128],{"class":221}," -o",[201,1130,1131],{"class":238}," myapp.exe",[201,1133,1134],{"class":221}," \\\n",[201,1136,1137,1140,1143],{"class":203,"line":646},[201,1138,1139],{"class":221},"    -ldflags=",[201,1141,1142],{"class":238},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[201,1144,1134],{"class":221},[201,1146,1147],{"class":203,"line":651},[201,1148,1149],{"class":238},"    main.go\n",[12,1151,1152],{"id":1152},"踩坑记录",[499,1154,1156],{"id":1155},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[17,1158,1159,1160,1162,1163,1165],{},"首次实现时用了 Gin 的 ",[198,1161,1004],{}," 兜底，但部分静态资源返回 404。排查发现 ",[198,1164,1004],{}," 在某些路由未显式注册时不会按预期触发。",[17,1167,1168,1171,1172,1175,1176,1179,1180,1182],{},[21,1169,1170],{},"解决","：显式注册 ",[198,1173,1174],{},"\u002Fassets\u002F*filepath","、",[198,1177,1178],{},"\u002Ffavicon.ico"," 等路由，",[198,1181,1004],{}," 仅作为兜底。",[499,1184,1186,1187,1189,1190,1193],{"id":1185},"坑二goembed-排除-_-开头文件","坑二：",[198,1188,489],{}," 排除 ",[198,1191,1192],{},"_"," 开头文件",[17,1195,1196,1197,1200,1201,1203,1204,1213],{},"这是最坑的。Vite 构建产出了一个 ",[198,1198,1199],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[198,1202,489],{}," 当模式为目录时，",[21,1205,1206,1207,1209,1210,1212],{},"硬编码排除了 ",[198,1208,1192],{}," 和 ",[198,1211,640],{}," 开头的文件","。",[191,1215,1217],{"className":505,"code":1216,"language":507,"meta":196,"style":196},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[198,1218,1219,1224],{"__ignoreMap":196},[201,1220,1221],{"class":203,"line":204},[201,1222,1223],{"class":207},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[201,1225,1226],{"class":203,"line":211},[201,1227,626],{"class":207},[17,1229,1230,1231,1213],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[198,1232,1233],{},"Unexpected token '\u003C'",[17,1235,1236,1238,1239,1241],{},[21,1237,1170],{},"：改 Vite 配置，去除了 ",[198,1240,1192],{}," 前缀：",[191,1243,1247],{"className":1244,"code":1245,"language":1246,"meta":196,"style":196},"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",[198,1248,1249,1254,1262,1269,1276,1284,1329,1334,1339,1344],{"__ignoreMap":196},[201,1250,1251],{"class":203,"line":204},[201,1252,1253],{"class":207},"\u002F\u002F vite.config.ts\n",[201,1255,1256,1259],{"class":203,"line":211},[201,1257,1258],{"class":291},"build",[201,1260,1261],{"class":228},": {\n",[201,1263,1264,1267],{"class":203,"line":232},[201,1265,1266],{"class":291},"  rollupOptions",[201,1268,1261],{"class":228},[201,1270,1271,1274],{"class":203,"line":245},[201,1272,1273],{"class":291},"    output",[201,1275,1261],{"class":228},[201,1277,1278,1281],{"class":203,"line":256},[201,1279,1280],{"class":291},"      chunkFileNames",[201,1282,1283],{"class":228},"(chunkInfo) {\n",[201,1285,1286,1289,1292,1295,1297,1300,1302,1305,1307,1310,1313,1316,1318,1320,1323,1326],{"class":203,"line":267},[201,1287,1288],{"class":214},"        return",[201,1290,1291],{"class":238}," `assets\u002F${",[201,1293,1294],{"class":228},"chunkInfo",[201,1296,640],{"class":238},[201,1298,1299],{"class":228},"name",[201,1301,640],{"class":238},[201,1303,1304],{"class":291},"replace",[201,1306,710],{"class":238},[201,1308,1309],{"class":238},"\u002F",[201,1311,1312],{"class":214},"^",[201,1314,1192],{"class":1315},"sPKmS",[201,1317,1309],{"class":238},[201,1319,338],{"class":238},[201,1321,1322],{"class":238},"''",[201,1324,1325],{"class":238},")",[201,1327,1328],{"class":238},"}-[hash].js`\n",[201,1330,1331],{"class":203,"line":273},[201,1332,1333],{"class":228},"      },\n",[201,1335,1336],{"class":203,"line":567},[201,1337,1338],{"class":228},"    },\n",[201,1340,1341],{"class":203,"line":577},[201,1342,1343],{"class":228},"  },\n",[201,1345,1346],{"class":203,"line":587},[201,1347,1348],{"class":228},"},\n",[17,1350,1351,1352,1354],{},"这样就避免产生 ",[198,1353,1192],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[499,1356,1358],{"id":1357},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[17,1360,1361,1362,1365],{},"多次 ",[198,1363,1364],{},"go build"," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[17,1367,1368,1370,1371,1374],{},[21,1369,1170],{},"：使用 ",[198,1372,1373],{},"go build -a"," 强制重新编译所有包。",[12,1376,1377],{"id":1377},"最终效果",[191,1379,1381],{"className":357,"code":1380,"language":359,"meta":196,"style":196},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[198,1382,1383,1394,1405],{"__ignoreMap":196},[201,1384,1385,1388,1391],{"class":203,"line":204},[201,1386,1387],{"class":291},"$",[201,1389,1390],{"class":238}," bash",[201,1392,1393],{"class":238}," build-standalone.sh\n",[201,1395,1396,1399,1402],{"class":203,"line":211},[201,1397,1398],{"class":238},"===",[201,1400,1401],{"class":238}," 构建成功",[201,1403,1404],{"class":238}," ===\n",[201,1406,1407,1410,1413],{"class":203,"line":232},[201,1408,1409],{"class":291},"输出:",[201,1411,1412],{"class":238}," service\u002Fmyapp.exe",[201,1414,1415],{"class":238},"  41M\n",[17,1417,1418],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[17,1420,1421],{},[21,1422,1423],{},"部署对比：",[35,1425,1426,1439],{},[38,1427,1428],{},[41,1429,1430,1433,1436],{},[44,1431,1432],{},"方式",[44,1434,1435],{},"文件数",[44,1437,1438],{},"操作",[51,1440,1441,1452],{},[41,1442,1443,1446,1449],{},[56,1444,1445],{},"旧方案",[56,1447,1448],{},"1 exe + 1 web 目录",[56,1450,1451],{},"解压→保持目录结构",[41,1453,1454,1457,1460],{},[56,1455,1456],{},"新方案",[56,1458,1459],{},"1 个 exe",[56,1461,1462],{},"直接扔到服务器运行",[12,1464,1465],{"id":1465},"总结",[17,1467,1468,1469,1471],{},"Go 的 ",[198,1470,489],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[1473,1474,1475,1482],"ol",{},[124,1476,1477,1478,1209,1480,1212],{},"嵌入目录时自动排除 ",[198,1479,1192],{},[198,1481,640],{},[124,1483,1484],{},"构建缓存可能导致 embed 不是最新的",[17,1486,1487],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[438,1489,1490],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .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 .sPKmS, html code.shiki .sPKmS{--shiki-light:#032F62;--shiki-dark:#DBEDFF}",{"title":196,"searchDepth":211,"depth":232,"links":1492},[1493,1494,1495,1500,1506,1507],{"id":470,"depth":211,"text":470},{"id":483,"depth":211,"text":483},{"id":497,"depth":211,"text":497,"children":1496},[1497,1498,1499],{"id":501,"depth":232,"text":502},{"id":895,"depth":232,"text":896},{"id":1010,"depth":232,"text":1011},{"id":1152,"depth":211,"text":1152,"children":1501},[1502,1503,1505],{"id":1155,"depth":232,"text":1156},{"id":1185,"depth":232,"text":1504},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":1357,"depth":232,"text":1358},{"id":1377,"depth":211,"text":1377},{"id":1465,"depth":211,"text":1465},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fgo-embed-frontend","2026-06-06T16:00:00",{"title":465,"description":1508},"posts\u002Fgo-embed-frontend\u002Findex",[1516,1517,1518,1519],"Go","Vue","编译","埋坑","aIlmmahxbuSBfpkrqwsdrbCQWjg7QHGt8tTc4iVnb5Q",{"id":1522,"title":1523,"author":7,"body":1524,"description":2294,"draft":451,"extension":452,"image":2295,"meta":2296,"navigation":394,"path":2297,"pinned":451,"published":2298,"seo":2299,"stem":2300,"tags":2301,"__hash__":2306},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":9,"value":1525,"toc":2280},[1526,1530,1533,1540,1666,1669,1672,1728,1731,1746,1749,1752,1759,1994,2001,2005,2008,2028,2031,2046,2049,2069,2072,2075,2082,2148,2151,2154,2158,2161,2213,2217,2238,2242,2245,2259,2262,2266,2269,2271,2274,2277],[12,1527,1529],{"id":1528},"dws-是什么","dws 是什么",[17,1531,1532],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[17,1534,1535,1536,1539],{},"它的定位很明确：",[21,1537,1538],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[191,1541,1543],{"className":357,"code":1542,"language":359,"meta":196,"style":196},"# 查看今天有什么会\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",[198,1544,1545,1550,1576,1580,1585,1607,1611,1616,1641,1645,1650],{"__ignoreMap":196},[201,1546,1547],{"class":203,"line":204},[201,1548,1549],{"class":207},"# 查看今天有什么会\n",[201,1551,1552,1555,1558,1561,1564,1567,1570,1573],{"class":203,"line":211},[201,1553,1554],{"class":291},"dws",[201,1556,1557],{"class":238}," calendar",[201,1559,1560],{"class":238}," event",[201,1562,1563],{"class":238}," list",[201,1565,1566],{"class":221}," --start",[201,1568,1569],{"class":238}," 2026-06-01",[201,1571,1572],{"class":221}," --end",[201,1574,1575],{"class":238}," 2026-06-01\n",[201,1577,1578],{"class":203,"line":232},[201,1579,527],{"emptyLinePlaceholder":394},[201,1581,1582],{"class":203,"line":245},[201,1583,1584],{"class":207},"# 给群里发条消息\n",[201,1586,1587,1589,1592,1595,1598,1601,1604],{"class":203,"line":256},[201,1588,1554],{"class":291},[201,1590,1591],{"class":238}," chat",[201,1593,1594],{"class":238}," send",[201,1596,1597],{"class":221}," --group-id",[201,1599,1600],{"class":238}," xxx",[201,1602,1603],{"class":221}," --content",[201,1605,1606],{"class":238}," \"今天发布推迟到下午3点\"\n",[201,1608,1609],{"class":203,"line":267},[201,1610,527],{"emptyLinePlaceholder":394},[201,1612,1613],{"class":203,"line":273},[201,1614,1615],{"class":207},"# 创建一个待办\n",[201,1617,1618,1620,1623,1626,1629,1632,1635,1638],{"class":203,"line":567},[201,1619,1554],{"class":291},[201,1621,1622],{"class":238}," todo",[201,1624,1625],{"class":238}," task",[201,1627,1628],{"class":238}," create",[201,1630,1631],{"class":221}," --subject",[201,1633,1634],{"class":238}," \"Review PR #123\"",[201,1636,1637],{"class":221}," --due-date",[201,1639,1640],{"class":238}," 2026-06-02\n",[201,1642,1643],{"class":203,"line":577},[201,1644,527],{"emptyLinePlaceholder":394},[201,1646,1647],{"class":203,"line":587},[201,1648,1649],{"class":207},"# 读取钉钉文档\n",[201,1651,1652,1654,1657,1660,1663],{"class":203,"line":597},[201,1653,1554],{"class":291},[201,1655,1656],{"class":238}," doc",[201,1658,1659],{"class":238}," read",[201,1661,1662],{"class":221}," --url",[201,1664,1665],{"class":238}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[12,1667,1668],{"id":1668},"安装",[17,1670,1671],{},"三种方式，选你喜欢的：",[191,1673,1675],{"className":357,"code":1674,"language":359,"meta":196,"style":196},"# 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",[198,1676,1677,1682,1695,1699,1704,1714,1718,1723],{"__ignoreMap":196},[201,1678,1679],{"class":203,"line":204},[201,1680,1681],{"class":207},"# npm（推荐，跨平台自动下载二进制）\n",[201,1683,1684,1687,1689,1692],{"class":203,"line":211},[201,1685,1686],{"class":291},"npm",[201,1688,1055],{"class":238},[201,1690,1691],{"class":221}," -g",[201,1693,1694],{"class":238}," dingtalk-workspace-cli\n",[201,1696,1697],{"class":203,"line":232},[201,1698,527],{"emptyLinePlaceholder":394},[201,1700,1701],{"class":203,"line":245},[201,1702,1703],{"class":207},"# Homebrew（macOS\u002FLinux）\n",[201,1705,1706,1709,1711],{"class":203,"line":256},[201,1707,1708],{"class":291},"brew",[201,1710,1055],{"class":238},[201,1712,1713],{"class":238}," dws\n",[201,1715,1716],{"class":203,"line":267},[201,1717,527],{"emptyLinePlaceholder":394},[201,1719,1720],{"class":203,"line":273},[201,1721,1722],{"class":207},"# 直接下载二进制\n",[201,1724,1725],{"class":203,"line":567},[201,1726,1727],{"class":207},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[17,1729,1730],{},"安装后认证：",[191,1732,1734],{"className":357,"code":1733,"language":359,"meta":196,"style":196},"dws auth login\n",[198,1735,1736],{"__ignoreMap":196},[201,1737,1738,1740,1743],{"class":203,"line":204},[201,1739,1554],{"class":291},[201,1741,1742],{"class":238}," auth",[201,1744,1745],{"class":238}," login\n",[17,1747,1748],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[12,1750,1751],{"id":1751},"覆盖的钉钉产品",[17,1753,1754,1755,1758],{},"这是最夸张的部分 — ",[21,1756,1757],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[35,1760,1761,1774],{},[38,1762,1763],{},[41,1764,1765,1768,1771],{},[44,1766,1767],{},"产品",[44,1769,1770],{},"命令数",[44,1772,1773],{},"说明",[51,1775,1776,1790,1804,1818,1832,1846,1860,1874,1887,1901,1914,1928,1942,1955,1969,1983],{},[41,1777,1778,1784,1787],{},[56,1779,1780,1781],{},"聊天 ",[198,1782,1783],{},"chat",[56,1785,1786],{},"57",[56,1788,1789],{},"消息收发、群管理、机器人、已读状态、Webhook",[41,1791,1792,1798,1801],{},[56,1793,1794,1795],{},"AI 表格 ",[198,1796,1797],{},"aitable",[56,1799,1800],{},"42",[56,1802,1803],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[41,1805,1806,1812,1815],{},[56,1807,1808,1809],{},"电子表格 ",[198,1810,1811],{},"sheet",[56,1813,1814],{},"34",[56,1816,1817],{},"工作表读写、行列操作、合并、筛选视图、导出",[41,1819,1820,1826,1829],{},[56,1821,1822,1823],{},"文档 ",[198,1824,1825],{},"doc",[56,1827,1828],{},"21",[56,1830,1831],{},"搜索、读写、块级编辑、评论、复制移动",[41,1833,1834,1840,1843],{},[56,1835,1836,1837],{},"会议纪要 ",[198,1838,1839],{},"minutes",[56,1841,1842],{},"19",[56,1844,1845],{},"列表、摘要、转写、思维导图、发言人、热词",[41,1847,1848,1854,1857],{},[56,1849,1850,1851],{},"日历 ",[198,1852,1853],{},"calendar",[56,1855,1856],{},"14",[56,1858,1859],{},"事件 CRUD、会议室、忙闲查询、时间建议",[41,1861,1862,1868,1871],{},[56,1863,1864,1865],{},"审批 ",[198,1866,1867],{},"oa",[56,1869,1870],{},"9",[56,1872,1873],{},"待办、发起、同意\u002F拒绝\u002F撤销",[41,1875,1876,1882,1884],{},[56,1877,1878,1879],{},"云盘 ",[198,1880,1881],{},"drive",[56,1883,1870],{},[56,1885,1886],{},"列表、下载、上传、创建目录",[41,1888,1889,1895,1898],{},[56,1890,1891,1892],{},"知识库 ",[198,1893,1894],{},"wiki",[56,1896,1897],{},"7",[56,1899,1900],{},"空间管理、成员管理",[41,1902,1903,1909,1911],{},[56,1904,1905,1906],{},"汇报 ",[198,1907,1908],{},"report",[56,1910,1897],{},[56,1912,1913],{},"日志\u002F日报\u002F周报，按模板创建、统计",[41,1915,1916,1922,1925],{},[56,1917,1918,1919],{},"待办 ",[198,1920,1921],{},"todo",[56,1923,1924],{},"6",[56,1926,1927],{},"创建、完成、删除，支持优先级和循环",[41,1929,1930,1936,1939],{},[56,1931,1932,1933],{},"邮件 ",[198,1934,1935],{},"mail",[56,1937,1938],{},"4",[56,1940,1941],{},"邮箱查询、邮件搜索、发送",[41,1943,1944,1950,1952],{},[56,1945,1946,1947],{},"考勤 ",[198,1948,1949],{},"attendance",[56,1951,1938],{},[56,1953,1954],{},"打卡记录、排班、汇总",[41,1956,1957,1963,1966],{},[56,1958,1959,1960],{},"通讯录 ",[198,1961,1962],{},"contact",[56,1964,1965],{},"3",[56,1967,1968],{},"用户搜索、部门查询",[41,1970,1971,1977,1980],{},[56,1972,1973,1974],{},"DING 消息 ",[198,1975,1976],{},"ding",[56,1978,1979],{},"2",[56,1981,1982],{},"发送\u002F撤回（应用内、短信、电话）",[41,1984,1985,1988,1991],{},[56,1986,1987],{},"其他",[56,1989,1990],{},"若干",[56,1992,1993],{},"AI 搜索、AI 应用、直播、开放平台文档",[17,1995,1996,1997,2000],{},"还有一个万能命令 ",[198,1998,1999],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[12,2002,2004],{"id":2003},"agent-skill-系统","Agent Skill 系统",[17,2006,2007],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[121,2009,2010,2016,2022],{},[124,2011,2012,2015],{},[21,2013,2014],{},"Claude Code"," — 通过 SKILL.md 自动加载",[124,2017,2018,2021],{},[21,2019,2020],{},"Cursor"," — 支持 MCP 协议接入",[124,2023,2024,2027],{},[21,2025,2026],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[17,2029,2030],{},"安装 skill：",[191,2032,2034],{"className":357,"code":2033,"language":359,"meta":196,"style":196},"dws skill setup\n",[198,2035,2036],{"__ignoreMap":196},[201,2037,2038,2040,2043],{"class":203,"line":204},[201,2039,1554],{"class":291},[201,2041,2042],{"class":238}," skill",[201,2044,2045],{"class":238}," setup\n",[17,2047,2048],{},"SKILL.md 里定义了：",[121,2050,2051,2060,2063,2066],{},[124,2052,2053,2054,2056,2057,2059],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[198,2055,1797],{},"，说\"打卡\"→ ",[198,2058,1949],{},"）",[124,2061,2062],{},"危险操作确认流程（删除、撤回等必须先确认）",[124,2064,2065],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[124,2067,2068],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[17,2070,2071],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[12,2073,2074],{"id":2074},"自动升级",[17,2076,2077,2078,2081],{},"内置 ",[198,2079,2080],{},"dws upgrade","，不需要重新下载安装包：",[191,2083,2085],{"className":357,"code":2084,"language":359,"meta":196,"style":196},"dws upgrade              # 交互式升级到最新版\ndws upgrade --check      # 仅检查是否有新版本\ndws upgrade --list       # 列出最近版本\ndws upgrade --version v1.0.5  # 升级到指定版本\ndws upgrade --rollback   # 回滚到上一版本\n",[198,2086,2087,2097,2109,2121,2136],{"__ignoreMap":196},[201,2088,2089,2091,2094],{"class":203,"line":204},[201,2090,1554],{"class":291},[201,2092,2093],{"class":238}," upgrade",[201,2095,2096],{"class":207},"              # 交互式升级到最新版\n",[201,2098,2099,2101,2103,2106],{"class":203,"line":211},[201,2100,1554],{"class":291},[201,2102,2093],{"class":238},[201,2104,2105],{"class":221}," --check",[201,2107,2108],{"class":207},"      # 仅检查是否有新版本\n",[201,2110,2111,2113,2115,2118],{"class":203,"line":232},[201,2112,1554],{"class":291},[201,2114,2093],{"class":238},[201,2116,2117],{"class":221}," --list",[201,2119,2120],{"class":207},"       # 列出最近版本\n",[201,2122,2123,2125,2127,2130,2133],{"class":203,"line":245},[201,2124,1554],{"class":291},[201,2126,2093],{"class":238},[201,2128,2129],{"class":221}," --version",[201,2131,2132],{"class":238}," v1.0.5",[201,2134,2135],{"class":207},"  # 升级到指定版本\n",[201,2137,2138,2140,2142,2145],{"class":203,"line":256},[201,2139,1554],{"class":291},[201,2141,2093],{"class":238},[201,2143,2144],{"class":221}," --rollback",[201,2146,2147],{"class":207},"   # 回滚到上一版本\n",[17,2149,2150],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[12,2152,2153],{"id":2153},"亮点",[499,2155,2157],{"id":2156},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[17,2159,2160],{},"这可能是目前对 AI 最友好的 CLI 工具：",[121,2162,2163,2184,2194,2203],{},[124,2164,2165,2168,2169,2172,2173,2176,2177,2172,2180,2183],{},[21,2166,2167],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[198,2170,2171],{},"--userId"," 应该是 ",[198,2174,2175],{},"--user-id","，",[198,2178,2179],{},"--limit100",[198,2181,2182],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[124,2185,2186,2189,2190,2193],{},[21,2187,2188],{},"Schema 发现"," — ",[198,2191,2192],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[124,2195,2196,2189,2199,2202],{},[21,2197,2198],{},"jq 过滤",[198,2200,2201],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[124,2204,2205,2208,2209,2212],{},[21,2206,2207],{},"JSON 输出"," — 所有命令支持 ",[198,2210,2211],{},"--format json","，方便程序解析",[499,2214,2216],{"id":2215},"_2-安全设计","2. 安全设计",[121,2218,2219,2222,2225,2228,2235],{},[124,2220,2221],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[124,2223,2224],{},"域名白名单，只能访问钉钉官方 API",[124,2226,2227],{},"强制 HTTPS",[124,2229,2230,2231,2234],{},"写操作必须显式 ",[198,2232,2233],{},"--yes"," 确认",[124,2236,2237],{},"完整审计链",[499,2239,2241],{"id":2240},"_3-发现驱动架构","3. 发现驱动架构",[17,2243,2244],{},"不是把所有命令硬编码在代码里，而是：",[1473,2246,2247,2250,2253,2256],{},[124,2248,2249],{},"从钉钉 MCP 服务器注册中心拉取元数据",[124,2251,2252],{},"运行时发现服务端能力",[124,2254,2255],{},"转换成统一的中间表示（IR）",[124,2257,2258],{},"动态挂载到命令树",[17,2260,2261],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[499,2263,2265],{"id":2264},"_4-多平台分发","4. 多平台分发",[17,2267,2268],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[12,2270,1465],{"id":1465},[17,2272,2273],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[17,2275,2276],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[438,2278,2279],{},"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":196,"searchDepth":211,"depth":232,"links":2281},[2282,2283,2284,2285,2286,2287,2293],{"id":1528,"depth":211,"text":1529},{"id":1668,"depth":211,"text":1668},{"id":1751,"depth":211,"text":1751},{"id":2003,"depth":211,"text":2004},{"id":2074,"depth":211,"text":2074},{"id":2153,"depth":211,"text":2153,"children":2288},[2289,2290,2291,2292],{"id":2156,"depth":232,"text":2157},{"id":2215,"depth":232,"text":2216},{"id":2240,"depth":232,"text":2241},{"id":2264,"depth":232,"text":2265},{"id":1465,"depth":211,"text":1465},"介绍阿里巴巴官方的 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":1523,"description":2294},"posts\u002Fdws-dingtalk-cli\u002Findex",[2302,2303,2304,1516,2305],"钉钉","CLI","AI Agent","效率工具","gHTBV8acc2BAq3CY7qgD7Bk2eJoJB_EFnGUrKFQE_PY",{"id":2308,"title":2309,"author":7,"body":2310,"description":4347,"draft":451,"extension":452,"image":4348,"meta":4349,"navigation":394,"path":4350,"pinned":451,"published":4351,"seo":4352,"stem":4353,"tags":4354,"__hash__":4358},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",{"type":9,"value":2311,"toc":4328},[2312,2316,2319,2322,2326,2333,2339,2342,2345,2365,2368,2372,2396,2400,2497,2510,2514,2676,2679,2893,2897,2908,3007,3014,3018,3288,3291,3371,3375,3378,3381,3392,3396,3402,3765,3955,3958,4014,4017,4171,4174,4177,4249,4251,4316,4325],[12,2313,2315],{"id":2314},"什么是-mcp","什么是 MCP",[17,2317,2318],{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[17,2320,2321],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[12,2323,2325],{"id":2324},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[17,2327,2328,2329,2332],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[198,2330,2331],{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[17,2334,2335,2336],{},"核心思路：",[21,2337,2338],{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[12,2340,2341],{"id":2341},"需求分析",[17,2343,2344],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[1473,2346,2347,2353,2359],{},[124,2348,2349,2352],{},[21,2350,2351],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[124,2354,2355,2358],{},[21,2356,2357],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[124,2360,2361,2364],{},[21,2362,2363],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[12,2366,2367],{"id":2367},"实现步骤",[499,2369,2371],{"id":2370},"_1-安装依赖","1. 安装依赖",[191,2373,2375],{"className":357,"code":2374,"language":359,"meta":196,"style":196},"go get github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\ngo get github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[198,2376,2377,2387],{"__ignoreMap":196},[201,2378,2379,2381,2384],{"class":203,"line":204},[201,2380,507],{"class":291},[201,2382,2383],{"class":238}," get",[201,2385,2386],{"class":238}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[201,2388,2389,2391,2393],{"class":203,"line":211},[201,2390,507],{"class":291},[201,2392,2383],{"class":238},[201,2394,2395],{"class":238}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[499,2397,2399],{"id":2398},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[191,2401,2403],{"className":505,"code":2402,"language":507,"meta":196,"style":196},"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",[198,2404,2405,2416,2420,2436,2447,2458,2469,2478,2493],{"__ignoreMap":196},[201,2406,2407,2409,2411,2414],{"class":203,"line":204},[201,2408,532],{"class":214},[201,2410,913],{"class":238},[201,2412,2413],{"class":291},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[201,2415,546],{"class":238},[201,2417,2418],{"class":203,"line":211},[201,2419,527],{"emptyLinePlaceholder":394},[201,2421,2422,2425,2427,2430,2433],{"class":203,"line":232},[201,2423,2424],{"class":228},"mcpServer ",[201,2426,679],{"class":214},[201,2428,2429],{"class":228}," server.",[201,2431,2432],{"class":291},"NewMCPServer",[201,2434,2435],{"class":228},"(\n",[201,2437,2438,2441,2444],{"class":203,"line":245},[201,2439,2440],{"class":238},"    \"my-app\"",[201,2442,2443],{"class":228},",    ",[201,2445,2446],{"class":207},"\u002F\u002F 服务器名称\n",[201,2448,2449,2452,2455],{"class":203,"line":256},[201,2450,2451],{"class":238},"    \"1.0.0\"",[201,2453,2454],{"class":228},",     ",[201,2456,2457],{"class":207},"\u002F\u002F 版本\n",[201,2459,2460,2463,2466],{"class":203,"line":267},[201,2461,2462],{"class":228},"    server.",[201,2464,2465],{"class":291},"WithLogging",[201,2467,2468],{"class":228},"(),\n",[201,2470,2471,2473,2476],{"class":203,"line":273},[201,2472,2462],{"class":228},[201,2474,2475],{"class":291},"WithRecovery",[201,2477,2468],{"class":228},[201,2479,2480,2482,2485,2487,2490],{"class":203,"line":567},[201,2481,2462],{"class":228},[201,2483,2484],{"class":291},"WithInstructions",[201,2486,710],{"class":228},[201,2488,2489],{"class":238},"\"这是一个 MCP 服务器示例\"",[201,2491,2492],{"class":228},"),\n",[201,2494,2495],{"class":203,"line":577},[201,2496,615],{"class":228},[17,2498,2499,2501,2502,2505,2506,2509],{},[198,2500,2432],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[198,2503,2504],{},"tools\u002Flist","）和工具调用（",[198,2507,2508],{},"tools\u002Fcall","）的协议逻辑。",[499,2511,2513],{"id":2512},"_3-注册工具","3. 注册工具",[191,2515,2517],{"className":505,"code":2516,"language":507,"meta":196,"style":196},"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",[198,2518,2519,2530,2534,2539,2559,2574,2578,2582,2587,2646,2651,2671],{"__ignoreMap":196},[201,2520,2521,2523,2525,2528],{"class":203,"line":204},[201,2522,532],{"class":214},[201,2524,913],{"class":238},[201,2526,2527],{"class":291},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[201,2529,546],{"class":238},[201,2531,2532],{"class":203,"line":211},[201,2533,527],{"emptyLinePlaceholder":394},[201,2535,2536],{"class":203,"line":232},[201,2537,2538],{"class":207},"\u002F\u002F 定义工具\n",[201,2540,2541,2544,2546,2549,2552,2554,2557],{"class":203,"line":245},[201,2542,2543],{"class":228},"listTool ",[201,2545,679],{"class":214},[201,2547,2548],{"class":228}," mcp.",[201,2550,2551],{"class":291},"NewTool",[201,2553,710],{"class":228},[201,2555,2556],{"class":238},"\"list_items\"",[201,2558,242],{"class":228},[201,2560,2561,2564,2567,2569,2572],{"class":203,"line":256},[201,2562,2563],{"class":228},"    mcp.",[201,2565,2566],{"class":291},"WithDescription",[201,2568,710],{"class":228},[201,2570,2571],{"class":238},"\"列出所有项目\"",[201,2573,2492],{"class":228},[201,2575,2576],{"class":203,"line":267},[201,2577,615],{"class":228},[201,2579,2580],{"class":203,"line":273},[201,2581,527],{"emptyLinePlaceholder":394},[201,2583,2584],{"class":203,"line":567},[201,2585,2586],{"class":207},"\u002F\u002F 注册处理器\n",[201,2588,2589,2592,2595,2598,2600,2602,2605,2608,2610,2612,2614,2617,2620,2622,2625,2628,2631,2634,2636,2639,2641,2644],{"class":203,"line":577},[201,2590,2591],{"class":228},"mcpServer.",[201,2593,2594],{"class":291},"AddTool",[201,2596,2597],{"class":228},"(listTool, ",[201,2599,654],{"class":214},[201,2601,710],{"class":228},[201,2603,2604],{"class":713},"ctx",[201,2606,2607],{"class":291}," context",[201,2609,640],{"class":228},[201,2611,724],{"class":291},[201,2613,338],{"class":228},[201,2615,2616],{"class":713},"req",[201,2618,2619],{"class":291}," mcp",[201,2621,640],{"class":228},[201,2623,2624],{"class":291},"CallToolRequest",[201,2626,2627],{"class":228},") (",[201,2629,2630],{"class":214},"*",[201,2632,2633],{"class":291},"mcp",[201,2635,640],{"class":228},[201,2637,2638],{"class":291},"CallToolResult",[201,2640,338],{"class":228},[201,2642,2643],{"class":214},"error",[201,2645,727],{"class":228},[201,2647,2648],{"class":203,"line":587},[201,2649,2650],{"class":207},"    \u002F\u002F 业务逻辑\n",[201,2652,2653,2655,2657,2660,2662,2665,2668],{"class":203,"line":597},[201,2654,704],{"class":214},[201,2656,2548],{"class":228},[201,2658,2659],{"class":291},"NewToolResultText",[201,2661,710],{"class":228},[201,2663,2664],{"class":238},"\"返回结果\"",[201,2666,2667],{"class":228},"), ",[201,2669,2670],{"class":221},"nil\n",[201,2672,2673],{"class":203,"line":602},[201,2674,2675],{"class":228},"})\n",[17,2677,2678],{},"带参数的工具：",[191,2680,2682],{"className":505,"code":2681,"language":507,"meta":196,"style":196},"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",[198,2683,2684,2702,2715,2729,2739,2753,2758,2762,2766,2813,2832,2845,2863,2867,2872,2889],{"__ignoreMap":196},[201,2685,2686,2689,2691,2693,2695,2697,2700],{"class":203,"line":204},[201,2687,2688],{"class":228},"getTool ",[201,2690,679],{"class":214},[201,2692,2548],{"class":228},[201,2694,2551],{"class":291},[201,2696,710],{"class":228},[201,2698,2699],{"class":238},"\"get_item\"",[201,2701,242],{"class":228},[201,2703,2704,2706,2708,2710,2713],{"class":203,"line":211},[201,2705,2563],{"class":228},[201,2707,2566],{"class":291},[201,2709,710],{"class":228},[201,2711,2712],{"class":238},"\"获取指定项目的详情\"",[201,2714,2492],{"class":228},[201,2716,2717,2719,2722,2724,2727],{"class":203,"line":232},[201,2718,2563],{"class":228},[201,2720,2721],{"class":291},"WithString",[201,2723,710],{"class":228},[201,2725,2726],{"class":238},"\"item_id\"",[201,2728,242],{"class":228},[201,2730,2731,2734,2737],{"class":203,"line":245},[201,2732,2733],{"class":228},"        mcp.",[201,2735,2736],{"class":291},"Required",[201,2738,2468],{"class":228},[201,2740,2741,2743,2746,2748,2751],{"class":203,"line":256},[201,2742,2733],{"class":228},[201,2744,2745],{"class":291},"Description",[201,2747,710],{"class":228},[201,2749,2750],{"class":238},"\"项目 ID\"",[201,2752,2492],{"class":228},[201,2754,2755],{"class":203,"line":267},[201,2756,2757],{"class":228},"    ),\n",[201,2759,2760],{"class":203,"line":273},[201,2761,615],{"class":228},[201,2763,2764],{"class":203,"line":567},[201,2765,527],{"emptyLinePlaceholder":394},[201,2767,2768,2770,2772,2775,2777,2779,2781,2783,2785,2787,2789,2791,2793,2795,2797,2799,2801,2803,2805,2807,2809,2811],{"class":203,"line":577},[201,2769,2591],{"class":228},[201,2771,2594],{"class":291},[201,2773,2774],{"class":228},"(getTool, ",[201,2776,654],{"class":214},[201,2778,710],{"class":228},[201,2780,2604],{"class":713},[201,2782,2607],{"class":291},[201,2784,640],{"class":228},[201,2786,724],{"class":291},[201,2788,338],{"class":228},[201,2790,2616],{"class":713},[201,2792,2619],{"class":291},[201,2794,640],{"class":228},[201,2796,2624],{"class":291},[201,2798,2627],{"class":228},[201,2800,2630],{"class":214},[201,2802,2633],{"class":291},[201,2804,640],{"class":228},[201,2806,2638],{"class":291},[201,2808,338],{"class":228},[201,2810,2643],{"class":214},[201,2812,727],{"class":228},[201,2814,2815,2818,2820,2823,2826,2828,2830],{"class":203,"line":587},[201,2816,2817],{"class":228},"    itemID, err ",[201,2819,679],{"class":214},[201,2821,2822],{"class":228}," req.",[201,2824,2825],{"class":291},"RequireString",[201,2827,710],{"class":228},[201,2829,2726],{"class":238},[201,2831,615],{"class":228},[201,2833,2834,2837,2839,2841,2843],{"class":203,"line":597},[201,2835,2836],{"class":214},"    if",[201,2838,813],{"class":228},[201,2840,816],{"class":214},[201,2842,819],{"class":221},[201,2844,229],{"class":228},[201,2846,2847,2849,2851,2854,2856,2859,2861],{"class":203,"line":602},[201,2848,1288],{"class":214},[201,2850,2548],{"class":228},[201,2852,2853],{"class":291},"NewToolResultError",[201,2855,710],{"class":228},[201,2857,2858],{"class":238},"\"缺少 item_id 参数\"",[201,2860,2667],{"class":228},[201,2862,2670],{"class":221},[201,2864,2865],{"class":203,"line":612},[201,2866,887],{"class":228},[201,2868,2869],{"class":203,"line":618},[201,2870,2871],{"class":207},"    \u002F\u002F 查询并返回\n",[201,2873,2874,2876,2878,2880,2882,2885,2887],{"class":203,"line":623},[201,2875,704],{"class":214},[201,2877,2548],{"class":228},[201,2879,2659],{"class":291},[201,2881,710],{"class":228},[201,2883,2884],{"class":238},"\"项目详情...\"",[201,2886,2667],{"class":228},[201,2888,2670],{"class":221},[201,2890,2891],{"class":203,"line":629},[201,2892,2675],{"class":228},[499,2894,2896],{"id":2895},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[17,2898,2899,2900,2903,2904,2907],{},"关键一步：用 ",[198,2901,2902],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[198,2905,2906],{},"gin.WrapH"," 适配 Gin。",[191,2909,2911],{"className":505,"code":2910,"language":507,"meta":196,"style":196},"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",[198,2912,2913,2923,2927,2942,2956,2973,2977,2981,2986],{"__ignoreMap":196},[201,2914,2915,2917,2919,2921],{"class":203,"line":204},[201,2916,532],{"class":214},[201,2918,913],{"class":238},[201,2920,2413],{"class":291},[201,2922,546],{"class":238},[201,2924,2925],{"class":203,"line":211},[201,2926,527],{"emptyLinePlaceholder":394},[201,2928,2929,2932,2934,2936,2939],{"class":203,"line":232},[201,2930,2931],{"class":228},"mcpHTTPServer ",[201,2933,679],{"class":214},[201,2935,2429],{"class":228},[201,2937,2938],{"class":291},"NewStreamableHTTPServer",[201,2940,2941],{"class":228},"(mcpServer,\n",[201,2943,2944,2946,2949,2951,2954],{"class":203,"line":245},[201,2945,2462],{"class":228},[201,2947,2948],{"class":291},"WithEndpointPath",[201,2950,710],{"class":228},[201,2952,2953],{"class":238},"\"\u002Fmcp\"",[201,2955,2492],{"class":228},[201,2957,2958,2960,2963,2965,2968,2970],{"class":203,"line":256},[201,2959,2462],{"class":228},[201,2961,2962],{"class":291},"WithHeartbeatInterval",[201,2964,710],{"class":228},[201,2966,2967],{"class":221},"30",[201,2969,2630],{"class":214},[201,2971,2972],{"class":228},"time.Second),\n",[201,2974,2975],{"class":203,"line":267},[201,2976,615],{"class":228},[201,2978,2979],{"class":203,"line":273},[201,2980,527],{"emptyLinePlaceholder":394},[201,2982,2983],{"class":203,"line":567},[201,2984,2985],{"class":207},"\u002F\u002F 挂载到 Gin\n",[201,2987,2988,2991,2994,2996,2998,3001,3004],{"class":203,"line":577},[201,2989,2990],{"class":228},"r.",[201,2992,2993],{"class":291},"Any",[201,2995,710],{"class":228},[201,2997,2953],{"class":238},[201,2999,3000],{"class":228},", gin.",[201,3002,3003],{"class":291},"WrapH",[201,3005,3006],{"class":228},"(mcpHTTPServer))\n",[17,3008,3009,3010,3013],{},"就这样，",[198,3011,3012],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[499,3015,3017],{"id":3016},"_5-完整的服务启动代码","5. 完整的服务启动代码",[191,3019,3021],{"className":505,"code":3020,"language":507,"meta":196,"style":196},"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",[198,3022,3023,3033,3048,3052,3057,3080,3089,3097,3102,3106,3111,3119,3123,3128,3141,3153,3167,3171,3189,3193,3198,3227,3261,3266,3270,3284],{"__ignoreMap":196},[201,3024,3025,3027,3030],{"class":203,"line":204},[201,3026,654],{"class":214},[201,3028,3029],{"class":291}," main",[201,3031,3032],{"class":228},"() {\n",[201,3034,3035,3038,3040,3043,3046],{"class":203,"line":211},[201,3036,3037],{"class":228},"    r ",[201,3039,679],{"class":214},[201,3041,3042],{"class":228}," gin.",[201,3044,3045],{"class":291},"Default",[201,3047,943],{"class":228},[201,3049,3050],{"class":203,"line":232},[201,3051,527],{"emptyLinePlaceholder":394},[201,3053,3054],{"class":203,"line":245},[201,3055,3056],{"class":207},"    \u002F\u002F 创建 MCP 服务器\n",[201,3058,3059,3062,3064,3066,3068,3070,3073,3075,3078],{"class":203,"line":256},[201,3060,3061],{"class":228},"    mcpServer ",[201,3063,679],{"class":214},[201,3065,2429],{"class":228},[201,3067,2432],{"class":291},[201,3069,710],{"class":228},[201,3071,3072],{"class":238},"\"my-app\"",[201,3074,338],{"class":228},[201,3076,3077],{"class":238},"\"1.0.0\"",[201,3079,242],{"class":228},[201,3081,3082,3085,3087],{"class":203,"line":267},[201,3083,3084],{"class":228},"        server.",[201,3086,2465],{"class":291},[201,3088,2468],{"class":228},[201,3090,3091,3093,3095],{"class":203,"line":273},[201,3092,3084],{"class":228},[201,3094,2475],{"class":291},[201,3096,2468],{"class":228},[201,3098,3099],{"class":203,"line":567},[201,3100,3101],{"class":228},"    )\n",[201,3103,3104],{"class":203,"line":577},[201,3105,527],{"emptyLinePlaceholder":394},[201,3107,3108],{"class":203,"line":587},[201,3109,3110],{"class":207},"    \u002F\u002F 注册工具\n",[201,3112,3113,3116],{"class":203,"line":597},[201,3114,3115],{"class":291},"    registerTools",[201,3117,3118],{"class":228},"(mcpServer)\n",[201,3120,3121],{"class":203,"line":602},[201,3122,527],{"emptyLinePlaceholder":394},[201,3124,3125],{"class":203,"line":612},[201,3126,3127],{"class":207},"    \u002F\u002F 挂载到 Gin\n",[201,3129,3130,3133,3135,3137,3139],{"class":203,"line":618},[201,3131,3132],{"class":228},"    mcpHTTP ",[201,3134,679],{"class":214},[201,3136,2429],{"class":228},[201,3138,2938],{"class":291},[201,3140,2941],{"class":228},[201,3142,3143,3145,3147,3149,3151],{"class":203,"line":623},[201,3144,3084],{"class":228},[201,3146,2948],{"class":291},[201,3148,710],{"class":228},[201,3150,2953],{"class":238},[201,3152,2492],{"class":228},[201,3154,3155,3157,3159,3161,3163,3165],{"class":203,"line":629},[201,3156,3084],{"class":228},[201,3158,2962],{"class":291},[201,3160,710],{"class":228},[201,3162,2967],{"class":221},[201,3164,2630],{"class":214},[201,3166,2972],{"class":228},[201,3168,3169],{"class":203,"line":646},[201,3170,3101],{"class":228},[201,3172,3173,3176,3178,3180,3182,3184,3186],{"class":203,"line":651},[201,3174,3175],{"class":228},"    r.",[201,3177,2993],{"class":291},[201,3179,710],{"class":228},[201,3181,2953],{"class":238},[201,3183,3000],{"class":228},[201,3185,3003],{"class":291},[201,3187,3188],{"class":228},"(mcpHTTP))\n",[201,3190,3191],{"class":203,"line":673},[201,3192,527],{"emptyLinePlaceholder":394},[201,3194,3195],{"class":203,"line":696},[201,3196,3197],{"class":207},"    \u002F\u002F 其他业务路由\n",[201,3199,3200,3202,3204,3206,3209,3211,3213,3215,3217,3219,3221,3223,3225],{"class":203,"line":701},[201,3201,3175],{"class":228},[201,3203,951],{"class":291},[201,3205,710],{"class":228},[201,3207,3208],{"class":238},"\"\u002Fapi\u002Fhealth\"",[201,3210,338],{"class":228},[201,3212,654],{"class":214},[201,3214,710],{"class":228},[201,3216,714],{"class":713},[201,3218,717],{"class":214},[201,3220,663],{"class":291},[201,3222,640],{"class":228},[201,3224,724],{"class":291},[201,3226,727],{"class":228},[201,3228,3229,3231,3234,3236,3239,3241,3243,3245,3248,3251,3254,3256,3259],{"class":203,"line":730},[201,3230,863],{"class":228},[201,3232,3233],{"class":291},"JSON",[201,3235,710],{"class":228},[201,3237,3238],{"class":221},"200",[201,3240,338],{"class":228},[201,3242,663],{"class":291},[201,3244,640],{"class":228},[201,3246,3247],{"class":291},"H",[201,3249,3250],{"class":228},"{",[201,3252,3253],{"class":238},"\"status\"",[201,3255,301],{"class":228},[201,3257,3258],{"class":238},"\"ok\"",[201,3260,2675],{"class":228},[201,3262,3263],{"class":203,"line":752},[201,3264,3265],{"class":228},"    })\n",[201,3267,3268],{"class":203,"line":769},[201,3269,527],{"emptyLinePlaceholder":394},[201,3271,3272,3274,3277,3279,3282],{"class":203,"line":781},[201,3273,3175],{"class":228},[201,3275,3276],{"class":291},"Run",[201,3278,710],{"class":228},[201,3280,3281],{"class":238},"\":8080\"",[201,3283,615],{"class":228},[201,3285,3286],{"class":203,"line":787},[201,3287,276],{"class":228},[17,3289,3290],{},"此时启动服务，用 curl 测试：",[191,3292,3294],{"className":357,"code":3293,"language":359,"meta":196,"style":196},"# 发现工具列表\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",[198,3295,3296,3301,3317,3327,3335,3339,3344,3356,3364],{"__ignoreMap":196},[201,3297,3298],{"class":203,"line":204},[201,3299,3300],{"class":207},"# 发现工具列表\n",[201,3302,3303,3306,3309,3312,3315],{"class":203,"line":211},[201,3304,3305],{"class":291},"curl",[201,3307,3308],{"class":221}," -X",[201,3310,3311],{"class":238}," POST",[201,3313,3314],{"class":238}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[201,3316,1134],{"class":221},[201,3318,3319,3322,3325],{"class":203,"line":232},[201,3320,3321],{"class":221},"  -H",[201,3323,3324],{"class":238}," \"Content-Type: application\u002Fjson\"",[201,3326,1134],{"class":221},[201,3328,3329,3332],{"class":203,"line":245},[201,3330,3331],{"class":221},"  -d",[201,3333,3334],{"class":238}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[201,3336,3337],{"class":203,"line":256},[201,3338,527],{"emptyLinePlaceholder":394},[201,3340,3341],{"class":203,"line":267},[201,3342,3343],{"class":207},"# 调用工具\n",[201,3345,3346,3348,3350,3352,3354],{"class":203,"line":273},[201,3347,3305],{"class":291},[201,3349,3308],{"class":221},[201,3351,3311],{"class":238},[201,3353,3314],{"class":238},[201,3355,1134],{"class":221},[201,3357,3358,3360,3362],{"class":203,"line":567},[201,3359,3321],{"class":221},[201,3361,3324],{"class":238},[201,3363,1134],{"class":221},[201,3365,3366,3368],{"class":203,"line":577},[201,3367,3331],{"class":221},[201,3369,3370],{"class":238}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[12,3372,3374],{"id":3373},"拓展接入鉴权","拓展：接入鉴权",[17,3376,3377],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[499,3379,3380],{"id":3380},"问题",[17,3382,3383,3384,3387,3388,3391],{},"MCP 的工具处理器签名为 ",[198,3385,3386],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[198,3389,3390],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[499,3393,3395],{"id":3394},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[17,3397,3398,3399,1213],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[198,3400,3401],{},"request.Context",[191,3403,3405],{"className":505,"code":3404,"language":507,"meta":196,"style":196},"\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",[198,3406,3407,3412,3440,3460,3480,3493,3526,3531,3535,3539,3563,3604,3633,3637,3641,3645,3666,3678,3707,3711,3715,3719,3734,3748,3757,3761],{"__ignoreMap":196},[201,3408,3409],{"class":203,"line":204},[201,3410,3411],{"class":207},"\u002F\u002F Gin 鉴权中间件\n",[201,3413,3414,3416,3419,3421,3424,3426,3429,3432,3434,3436,3438],{"class":203,"line":211},[201,3415,654],{"class":214},[201,3417,3418],{"class":291}," AuthRequired",[201,3420,710],{"class":228},[201,3422,3423],{"class":713},"authService",[201,3425,717],{"class":214},[201,3427,3428],{"class":291},"AuthService",[201,3430,3431],{"class":228},") ",[201,3433,663],{"class":291},[201,3435,640],{"class":228},[201,3437,668],{"class":291},[201,3439,229],{"class":228},[201,3441,3442,3444,3446,3448,3450,3452,3454,3456,3458],{"class":203,"line":232},[201,3443,704],{"class":214},[201,3445,707],{"class":214},[201,3447,710],{"class":228},[201,3449,714],{"class":713},[201,3451,717],{"class":214},[201,3453,663],{"class":291},[201,3455,640],{"class":228},[201,3457,724],{"class":291},[201,3459,727],{"class":228},[201,3461,3462,3465,3467,3470,3473,3475,3478],{"class":203,"line":245},[201,3463,3464],{"class":228},"        authHeader ",[201,3466,679],{"class":214},[201,3468,3469],{"class":228}," c.",[201,3471,3472],{"class":291},"GetHeader",[201,3474,710],{"class":228},[201,3476,3477],{"class":238},"\"Authorization\"",[201,3479,615],{"class":228},[201,3481,3482,3484,3487,3489,3491],{"class":203,"line":256},[201,3483,755],{"class":214},[201,3485,3486],{"class":228}," authHeader ",[201,3488,761],{"class":214},[201,3490,764],{"class":238},[201,3492,229],{"class":228},[201,3494,3495,3498,3501,3503,3506,3508,3510,3512,3514,3516,3519,3521,3524],{"class":203,"line":267},[201,3496,3497],{"class":228},"            c.",[201,3499,3500],{"class":291},"AbortWithStatusJSON",[201,3502,710],{"class":228},[201,3504,3505],{"class":221},"401",[201,3507,338],{"class":228},[201,3509,663],{"class":291},[201,3511,640],{"class":228},[201,3513,3247],{"class":291},[201,3515,3250],{"class":228},[201,3517,3518],{"class":238},"\"message\"",[201,3520,301],{"class":228},[201,3522,3523],{"class":238},"\"缺少认证令牌\"",[201,3525,2675],{"class":228},[201,3527,3528],{"class":203,"line":273},[201,3529,3530],{"class":214},"            return\n",[201,3532,3533],{"class":203,"line":567},[201,3534,784],{"class":228},[201,3536,3537],{"class":203,"line":577},[201,3538,527],{"emptyLinePlaceholder":394},[201,3540,3541,3544,3546,3548,3551,3554,3557,3559,3561],{"class":203,"line":587},[201,3542,3543],{"class":228},"        parts ",[201,3545,679],{"class":214},[201,3547,738],{"class":228},[201,3549,3550],{"class":291},"SplitN",[201,3552,3553],{"class":228},"(authHeader, ",[201,3555,3556],{"class":238},"\" \"",[201,3558,338],{"class":228},[201,3560,1979],{"class":221},[201,3562,615],{"class":228},[201,3564,3565,3567,3570,3573,3575,3578,3581,3584,3587,3590,3593,3596,3599,3602],{"class":203,"line":597},[201,3566,755],{"class":214},[201,3568,3569],{"class":291}," len",[201,3571,3572],{"class":228},"(parts) ",[201,3574,816],{"class":214},[201,3576,3577],{"class":221}," 2",[201,3579,3580],{"class":214}," ||",[201,3582,3583],{"class":214}," !",[201,3585,3586],{"class":228},"strings.",[201,3588,3589],{"class":291},"EqualFold",[201,3591,3592],{"class":228},"(parts[",[201,3594,3595],{"class":221},"0",[201,3597,3598],{"class":228},"], ",[201,3600,3601],{"class":238},"\"Bearer\"",[201,3603,727],{"class":228},[201,3605,3606,3608,3610,3612,3614,3616,3618,3620,3622,3624,3626,3628,3631],{"class":203,"line":602},[201,3607,3497],{"class":228},[201,3609,3500],{"class":291},[201,3611,710],{"class":228},[201,3613,3505],{"class":221},[201,3615,338],{"class":228},[201,3617,663],{"class":291},[201,3619,640],{"class":228},[201,3621,3247],{"class":291},[201,3623,3250],{"class":228},[201,3625,3518],{"class":238},[201,3627,301],{"class":228},[201,3629,3630],{"class":238},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[201,3632,2675],{"class":228},[201,3634,3635],{"class":203,"line":612},[201,3636,3530],{"class":214},[201,3638,3639],{"class":203,"line":618},[201,3640,784],{"class":228},[201,3642,3643],{"class":203,"line":623},[201,3644,527],{"emptyLinePlaceholder":394},[201,3646,3647,3650,3652,3655,3658,3660,3663],{"class":203,"line":629},[201,3648,3649],{"class":228},"        user, err ",[201,3651,679],{"class":214},[201,3653,3654],{"class":228}," authService.",[201,3656,3657],{"class":291},"GetUserByToken",[201,3659,3592],{"class":228},[201,3661,3662],{"class":221},"1",[201,3664,3665],{"class":228},"])\n",[201,3667,3668,3670,3672,3674,3676],{"class":203,"line":646},[201,3669,755],{"class":214},[201,3671,813],{"class":228},[201,3673,816],{"class":214},[201,3675,819],{"class":221},[201,3677,229],{"class":228},[201,3679,3680,3682,3684,3686,3688,3690,3692,3694,3696,3698,3700,3702,3705],{"class":203,"line":651},[201,3681,3497],{"class":228},[201,3683,3500],{"class":291},[201,3685,710],{"class":228},[201,3687,3505],{"class":221},[201,3689,338],{"class":228},[201,3691,663],{"class":291},[201,3693,640],{"class":228},[201,3695,3247],{"class":291},[201,3697,3250],{"class":228},[201,3699,3518],{"class":238},[201,3701,301],{"class":228},[201,3703,3704],{"class":238},"\"无效的认证令牌\"",[201,3706,2675],{"class":228},[201,3708,3709],{"class":203,"line":673},[201,3710,3530],{"class":214},[201,3712,3713],{"class":203,"line":696},[201,3714,784],{"class":228},[201,3716,3717],{"class":203,"line":701},[201,3718,527],{"emptyLinePlaceholder":394},[201,3720,3721,3723,3726,3728,3731],{"class":203,"line":730},[201,3722,863],{"class":228},[201,3724,3725],{"class":291},"Set",[201,3727,710],{"class":228},[201,3729,3730],{"class":238},"\"userID\"",[201,3732,3733],{"class":228},", user.ID)\n",[201,3735,3736,3738,3740,3742,3745],{"class":203,"line":752},[201,3737,863],{"class":228},[201,3739,3725],{"class":291},[201,3741,710],{"class":228},[201,3743,3744],{"class":238},"\"username\"",[201,3746,3747],{"class":228},", user.Username)\n",[201,3749,3750,3752,3755],{"class":203,"line":769},[201,3751,863],{"class":228},[201,3753,3754],{"class":291},"Next",[201,3756,943],{"class":228},[201,3758,3759],{"class":203,"line":781},[201,3760,887],{"class":228},[201,3762,3763],{"class":203,"line":787},[201,3764,276],{"class":228},[191,3766,3768],{"className":505,"code":3767,"language":507,"meta":196,"style":196},"\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",[198,3769,3770,3775,3786,3793,3806,3818,3822,3826,3843,3863,3877,3902,3924,3939,3947,3951],{"__ignoreMap":196},[201,3771,3772],{"class":203,"line":204},[201,3773,3774],{"class":207},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[201,3776,3777,3780,3783],{"class":203,"line":211},[201,3778,3779],{"class":214},"type",[201,3781,3782],{"class":291}," ctxKey",[201,3784,3785],{"class":214}," string\n",[201,3787,3788,3791],{"class":203,"line":232},[201,3789,3790],{"class":214},"const",[201,3792,535],{"class":228},[201,3794,3795,3798,3801,3803],{"class":203,"line":245},[201,3796,3797],{"class":221},"    ctxUserID",[201,3799,3800],{"class":291},"   ctxKey",[201,3802,225],{"class":214},[201,3804,3805],{"class":238}," \"userID\"\n",[201,3807,3808,3811,3813,3815],{"class":203,"line":256},[201,3809,3810],{"class":221},"    ctxUsername",[201,3812,3782],{"class":291},[201,3814,225],{"class":214},[201,3816,3817],{"class":238}," \"username\"\n",[201,3819,3820],{"class":203,"line":267},[201,3821,615],{"class":228},[201,3823,3824],{"class":203,"line":273},[201,3825,527],{"emptyLinePlaceholder":394},[201,3827,3828,3830,3833,3835,3837,3839,3841],{"class":203,"line":567},[201,3829,654],{"class":214},[201,3831,3832],{"class":291}," WithUserContext",[201,3834,660],{"class":228},[201,3836,663],{"class":291},[201,3838,640],{"class":228},[201,3840,668],{"class":291},[201,3842,229],{"class":228},[201,3844,3845,3847,3849,3851,3853,3855,3857,3859,3861],{"class":203,"line":577},[201,3846,704],{"class":214},[201,3848,707],{"class":214},[201,3850,710],{"class":228},[201,3852,714],{"class":713},[201,3854,717],{"class":214},[201,3856,663],{"class":291},[201,3858,640],{"class":228},[201,3860,724],{"class":291},[201,3862,727],{"class":228},[201,3864,3865,3868,3870,3873,3875],{"class":203,"line":587},[201,3866,3867],{"class":228},"        ctx ",[201,3869,679],{"class":214},[201,3871,3872],{"class":228}," c.Request.",[201,3874,724],{"class":291},[201,3876,943],{"class":228},[201,3878,3879,3881,3883,3886,3889,3892,3895,3897,3899],{"class":203,"line":597},[201,3880,3867],{"class":228},[201,3882,775],{"class":214},[201,3884,3885],{"class":228}," context.",[201,3887,3888],{"class":291},"WithValue",[201,3890,3891],{"class":228},"(ctx, ctxUserID, c.",[201,3893,3894],{"class":291},"GetUint",[201,3896,710],{"class":228},[201,3898,3730],{"class":238},[201,3900,3901],{"class":228},"))\n",[201,3903,3904,3906,3908,3910,3912,3915,3918,3920,3922],{"class":203,"line":602},[201,3905,3867],{"class":228},[201,3907,775],{"class":214},[201,3909,3885],{"class":228},[201,3911,3888],{"class":291},[201,3913,3914],{"class":228},"(ctx, ctxUsername, c.",[201,3916,3917],{"class":291},"GetString",[201,3919,710],{"class":228},[201,3921,3744],{"class":238},[201,3923,3901],{"class":228},[201,3925,3926,3929,3931,3933,3936],{"class":203,"line":612},[201,3927,3928],{"class":228},"        c.Request ",[201,3930,775],{"class":214},[201,3932,3872],{"class":228},[201,3934,3935],{"class":291},"WithContext",[201,3937,3938],{"class":228},"(ctx)\n",[201,3940,3941,3943,3945],{"class":203,"line":618},[201,3942,863],{"class":228},[201,3944,3754],{"class":291},[201,3946,943],{"class":228},[201,3948,3949],{"class":203,"line":623},[201,3950,887],{"class":228},[201,3952,3953],{"class":203,"line":629},[201,3954,276],{"class":228},[499,3956,3957],{"id":3957},"组合使用",[191,3959,3961],{"className":505,"code":3960,"language":507,"meta":196,"style":196},"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",[198,3962,3963,3975,3986,3997,4010],{"__ignoreMap":196},[201,3964,3965,3967,3969,3971,3973],{"class":203,"line":204},[201,3966,2990],{"class":228},[201,3968,2993],{"class":291},[201,3970,710],{"class":228},[201,3972,2953],{"class":238},[201,3974,242],{"class":228},[201,3976,3977,3980,3983],{"class":203,"line":211},[201,3978,3979],{"class":291},"    AuthRequired",[201,3981,3982],{"class":228},"(authService),  ",[201,3984,3985],{"class":207},"\u002F\u002F 1. 验证 Token\n",[201,3987,3988,3991,3994],{"class":203,"line":232},[201,3989,3990],{"class":291},"    WithUserContext",[201,3992,3993],{"class":228},"(),           ",[201,3995,3996],{"class":207},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[201,3998,3999,4002,4004,4007],{"class":203,"line":245},[201,4000,4001],{"class":228},"    gin.",[201,4003,3003],{"class":291},[201,4005,4006],{"class":228},"(mcpHTTP),         ",[201,4008,4009],{"class":207},"\u002F\u002F 3. MCP 处理\n",[201,4011,4012],{"class":203,"line":256},[201,4013,615],{"class":228},[17,4015,4016],{},"在工具处理器里就能拿到用户信息了：",[191,4018,4020],{"className":505,"code":4019,"language":507,"meta":196,"style":196},"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",[198,4021,4022,4078,4099,4108,4125,4129,4133,4149,4154,4167],{"__ignoreMap":196},[201,4023,4024,4026,4029,4032,4034,4037,4039,4042,4044,4046,4048,4050,4052,4054,4056,4058,4060,4062,4064,4066,4068,4070,4072,4074,4076],{"class":203,"line":204},[201,4025,654],{"class":214},[201,4027,4028],{"class":228}," (",[201,4030,4031],{"class":713},"h ",[201,4033,2630],{"class":214},[201,4035,4036],{"class":291},"Handler",[201,4038,3431],{"class":228},[201,4040,4041],{"class":291},"handleListItems",[201,4043,710],{"class":228},[201,4045,2604],{"class":713},[201,4047,2607],{"class":291},[201,4049,640],{"class":228},[201,4051,724],{"class":291},[201,4053,338],{"class":228},[201,4055,2616],{"class":713},[201,4057,2619],{"class":291},[201,4059,640],{"class":228},[201,4061,2624],{"class":291},[201,4063,2627],{"class":228},[201,4065,2630],{"class":214},[201,4067,2633],{"class":291},[201,4069,640],{"class":228},[201,4071,2638],{"class":291},[201,4073,338],{"class":228},[201,4075,2643],{"class":214},[201,4077,727],{"class":228},[201,4079,4080,4083,4085,4088,4091,4094,4097],{"class":203,"line":211},[201,4081,4082],{"class":228},"    userID, ok ",[201,4084,679],{"class":214},[201,4086,4087],{"class":228}," ctx.",[201,4089,4090],{"class":291},"Value",[201,4092,4093],{"class":228},"(ctxUserID).(",[201,4095,4096],{"class":214},"uint",[201,4098,615],{"class":228},[201,4100,4101,4103,4105],{"class":203,"line":232},[201,4102,2836],{"class":214},[201,4104,3583],{"class":214},[201,4106,4107],{"class":228},"ok {\n",[201,4109,4110,4112,4114,4116,4118,4121,4123],{"class":203,"line":245},[201,4111,1288],{"class":214},[201,4113,2548],{"class":228},[201,4115,2853],{"class":291},[201,4117,710],{"class":228},[201,4119,4120],{"class":238},"\"未认证\"",[201,4122,2667],{"class":228},[201,4124,2670],{"class":221},[201,4126,4127],{"class":203,"line":256},[201,4128,887],{"class":228},[201,4130,4131],{"class":203,"line":267},[201,4132,527],{"emptyLinePlaceholder":394},[201,4134,4135,4138,4140,4143,4146],{"class":203,"line":273},[201,4136,4137],{"class":228},"    items, _ ",[201,4139,679],{"class":214},[201,4141,4142],{"class":228}," h.service.",[201,4144,4145],{"class":291},"ListByUser",[201,4147,4148],{"class":228},"(userID)\n",[201,4150,4151],{"class":203,"line":567},[201,4152,4153],{"class":207},"    \u002F\u002F ...\n",[201,4155,4156,4158,4160,4162,4165],{"class":203,"line":577},[201,4157,704],{"class":214},[201,4159,2548],{"class":228},[201,4161,2659],{"class":291},[201,4163,4164],{"class":228},"(result), ",[201,4166,2670],{"class":221},[201,4168,4169],{"class":203,"line":587},[201,4170,276],{"class":228},[499,4172,4173],{"id":4173},"客户端配置",[17,4175,4176],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[191,4178,4182],{"className":4179,"code":4180,"language":4181,"meta":196,"style":196},"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",[198,4183,4184,4189,4196,4202,4214,4221,4231,4236,4240,4245],{"__ignoreMap":196},[201,4185,4186],{"class":203,"line":204},[201,4187,4188],{"class":228},"{\n",[201,4190,4191,4194],{"class":203,"line":211},[201,4192,4193],{"class":221},"  \"mcpServers\"",[201,4195,1261],{"class":228},[201,4197,4198,4200],{"class":203,"line":232},[201,4199,2440],{"class":221},[201,4201,1261],{"class":228},[201,4203,4204,4207,4209,4212],{"class":203,"line":245},[201,4205,4206],{"class":221},"      \"url\"",[201,4208,301],{"class":228},[201,4210,4211],{"class":238},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[201,4213,242],{"class":228},[201,4215,4216,4219],{"class":203,"line":256},[201,4217,4218],{"class":221},"      \"headers\"",[201,4220,1261],{"class":228},[201,4222,4223,4226,4228],{"class":203,"line":267},[201,4224,4225],{"class":221},"        \"Authorization\"",[201,4227,301],{"class":228},[201,4229,4230],{"class":238},"\"Bearer your-token-here\"\n",[201,4232,4233],{"class":203,"line":273},[201,4234,4235],{"class":228},"      }\n",[201,4237,4238],{"class":203,"line":567},[201,4239,887],{"class":228},[201,4241,4242],{"class":203,"line":577},[201,4243,4244],{"class":228},"  }\n",[201,4246,4247],{"class":203,"line":587},[201,4248,276],{"class":228},[12,4250,1465],{"id":1465},[35,4252,4253,4262],{},[38,4254,4255],{},[41,4256,4257,4260],{},[44,4258,4259],{},"步骤",[44,4261,1773],{},[51,4263,4264,4273,4285,4294,4303],{},[41,4265,4266,4270],{},[56,4267,4268],{},[198,4269,2432],{},[56,4271,4272],{},"创建 MCP 协议实例",[41,4274,4275,4282],{},[56,4276,4277,4279,4280],{},[198,4278,2551],{}," + ",[198,4281,2594],{},[56,4283,4284],{},"定义并注册工具",[41,4286,4287,4291],{},[56,4288,4289],{},[198,4290,2938],{},[56,4292,4293],{},"转为 HTTP Handler",[41,4295,4296,4300],{},[56,4297,4298],{},[198,4299,2906],{},[56,4301,4302],{},"挂载到 Gin 路由",[41,4304,4305,4313],{},[56,4306,4307,4279,4310],{},[198,4308,4309],{},"AuthRequired",[198,4311,4312],{},"WithUserContext",[56,4314,4315],{},"鉴权 + 用户信息桥接",[17,4317,4318,4319,4322,4323,1213],{},"核心就一句话：",[21,4320,4321],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[198,4324,3401],{},[438,4326,4327],{},"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 .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 .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}",{"title":196,"searchDepth":211,"depth":232,"links":4329},[4330,4331,4332,4333,4340,4346],{"id":2314,"depth":211,"text":2315},{"id":2324,"depth":211,"text":2325},{"id":2341,"depth":211,"text":2341},{"id":2367,"depth":211,"text":2367,"children":4334},[4335,4336,4337,4338,4339],{"id":2370,"depth":232,"text":2371},{"id":2398,"depth":232,"text":2399},{"id":2512,"depth":232,"text":2513},{"id":2895,"depth":232,"text":2896},{"id":3016,"depth":232,"text":3017},{"id":3373,"depth":211,"text":3374,"children":4341},[4342,4343,4344,4345],{"id":3380,"depth":232,"text":3380},{"id":3394,"depth":232,"text":3395},{"id":3957,"depth":232,"text":3957},{"id":4173,"depth":232,"text":4173},{"id":1465,"depth":211,"text":1465},"介绍如何使用 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":2309,"description":4347},"posts\u002Fgo-mcp-gin\u002Findex",[1516,4355,4356,2304,4357],"MCP","Gin","后端","hsvJkRE0ePPsoEmWVhOynhgvBdWZ_Z-vv8puDNRCQHs",{"id":4360,"title":4361,"author":4362,"body":4363,"description":4639,"draft":451,"extension":452,"image":4640,"meta":4641,"navigation":394,"path":4642,"pinned":451,"published":4643,"seo":4644,"stem":4645,"tags":4646,"__hash__":4650},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题","Nixus",{"type":9,"value":4364,"toc":4632},[4365,4368,4371,4386,4389,4392,4395,4436,4447,4454,4476,4483,4487,4490,4493,4496,4499,4522,4533,4594,4599,4612,4614,4620,4629],[12,4366,4367],{"id":4367},"问题描述",[17,4369,4370],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[121,4372,4373,4383],{},[124,4374,4375,4376,4379,4380],{},"首次通过客户端导航（点击 ",[198,4377,4378],{},"\u003CNuxtLink>","）跳转到某个页面时，",[21,4381,4382],{},"所有链接文字都会出现下划线",[124,4384,4385],{},"手动刷新页面后，下划线消失，样式恢复正常",[17,4387,4388],{},"这个问题在开发模式和生产构建后都会出现。",[12,4390,4391],{"id":4391},"原因分析",[17,4393,4394],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[191,4396,4400],{"className":4397,"code":4398,"language":4399,"meta":196,"style":196},"language-css shiki shiki-themes github-light github-dark","a {\n  color: inherit;\n  text-decoration: inherit;\n}\n","css",[198,4401,4402,4408,4421,4432],{"__ignoreMap":196},[201,4403,4404,4406],{"class":203,"line":204},[201,4405,422],{"class":297},[201,4407,229],{"class":228},[201,4409,4410,4413,4415,4418],{"class":203,"line":211},[201,4411,4412],{"class":221},"  color",[201,4414,301],{"class":228},[201,4416,4417],{"class":221},"inherit",[201,4419,4420],{"class":228},";\n",[201,4422,4423,4426,4428,4430],{"class":203,"line":232},[201,4424,4425],{"class":221},"  text-decoration",[201,4427,301],{"class":228},[201,4429,4417],{"class":221},[201,4431,4420],{"class":228},[201,4433,4434],{"class":203,"line":245},[201,4435,276],{"class":228},[17,4437,4438,4439,4442,4443,4446],{},"正常情况下，这会让链接继承父元素的 ",[198,4440,4441],{},"text-decoration","（通常是 ",[198,4444,4445],{},"none","），从而去掉浏览器默认的下划线。",[17,4448,4449,4450,4453],{},"问题出在 ",[21,4451,4452],{},"Nuxt 的客户端导航机制","：",[1473,4455,4456,4462,4465,4470,4473],{},[124,4457,4458,4459,4461],{},"用户点击 ",[198,4460,4378],{},"，Vue Router 拦截导航",[124,4463,4464],{},"新页面组件异步加载并渲染",[124,4466,4467],{},[21,4468,4469],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[124,4471,4472],{},"此时浏览器使用默认样式（链接带下划线）",[124,4474,4475],{},"CSS 加载完成后，下划线消失",[17,4477,4478,4479,4482],{},"这就是典型的 ",[21,4480,4481],{},"FOUC（Flash of Unstyled Content）"," 问题。",[12,4484,4486],{"id":4485},"为什么刷新就正常了","为什么刷新就正常了？",[17,4488,4489],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[17,4491,4492],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[12,4494,4495],{"id":4495},"解决方案",[17,4497,4498],{},"尝试了多种方案：",[121,4500,4501,4507,4513,4519],{},[124,4502,4503,4506],{},[198,4504,4505],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[124,4508,4509,4512],{},[198,4510,4511],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[124,4514,4515,4518],{},[198,4516,4517],{},"features.inlineStyles: true"," — 无效",[124,4520,4521],{},"页面过渡动画 — 只是掩盖，不能根治",[17,4523,4524,4525,4528,4529,4532],{},"最终有效的方案是在 ",[198,4526,4527],{},"app.vue"," 中用 ",[198,4530,4531],{},"!important"," 强制覆盖：",[191,4534,4538],{"className":4535,"code":4536,"language":4537,"meta":196,"style":196},"language-vue shiki shiki-themes github-light github-dark","\u003Cstyle>\na:not(.prose a) {\n  text-decoration-line: none !important;\n}\n\u003C\u002Fstyle>\n","vue",[198,4539,4540,4550,4567,4581,4585],{"__ignoreMap":196},[201,4541,4542,4545,4547],{"class":203,"line":204},[201,4543,4544],{"class":228},"\u003C",[201,4546,438],{"class":297},[201,4548,4549],{"class":228},">\n",[201,4551,4552,4554,4557,4559,4562,4565],{"class":203,"line":211},[201,4553,422],{"class":297},[201,4555,4556],{"class":291},":not",[201,4558,710],{"class":228},[201,4560,4561],{"class":291},".prose",[201,4563,4564],{"class":297}," a",[201,4566,727],{"class":228},[201,4568,4569,4572,4574,4576,4579],{"class":203,"line":232},[201,4570,4571],{"class":221},"  text-decoration-line",[201,4573,301],{"class":228},[201,4575,4445],{"class":221},[201,4577,4578],{"class":214}," !important",[201,4580,4420],{"class":228},[201,4582,4583],{"class":203,"line":245},[201,4584,276],{"class":228},[201,4586,4587,4590,4592],{"class":203,"line":256},[201,4588,4589],{"class":228},"\u003C\u002F",[201,4591,438],{"class":297},[201,4593,4549],{"class":228},[17,4595,4596],{},[21,4597,4598],{},"关键点：",[121,4600,4601,4606],{},[124,4602,4603,4605],{},[198,4604,4531],{}," 是为了对抗 Tailwind CSS layers 的优先级",[124,4607,4608,4611],{},[198,4609,4610],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[12,4613,1465],{"id":1465},[17,4615,4616,4617,4619],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[198,4618,4531],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[17,4621,4622,4623,4628],{},"如果你有更好的方案，欢迎在 ",[422,4624,4627],{"href":4625,"rel":4626},"https:\u002F\u002Fgithub.com\u002Fwishesl\u002FSuBlog\u002Fissues",[426],"Issues"," 提出！",[438,4630,4631],{},"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":196,"searchDepth":211,"depth":232,"links":4633},[4634,4635,4636,4637,4638],{"id":4367,"depth":211,"text":4367},{"id":4391,"depth":211,"text":4391},{"id":4485,"depth":211,"text":4486},{"id":4495,"depth":211,"text":4495},{"id":1465,"depth":211,"text":1465},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":4361,"description":4639},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[461,4647,4648,4649],"Tailwind CSS","CSS","踩坑","uD8rqIXc3kgBmbc47CsQbSv6XvsbCuIMLYSeBdu1wzQ",{"id":4652,"title":4653,"author":7,"body":4654,"description":5804,"draft":451,"extension":452,"image":5805,"meta":5806,"navigation":394,"path":5807,"pinned":451,"published":5808,"seo":5809,"stem":5810,"tags":5811,"__hash__":5813},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",{"type":9,"value":4655,"toc":5794},[4656,4659,4662,4665,4669,4672,4682,4685,4692,4756,4763,4766,4773,5039,5049,5053,5056,5059,5084,5098,5101,5124,5130,5133,5136,5139,5144,5151,5372,5377,5421,5425,5428,5435,5438,5488,5494,5722,5732,5734,5785,5788,5791],[4657,4658,470],"h1",{"id":470},[17,4660,4661],{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[17,4663,4664],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[4657,4666,4668],{"id":4667},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[12,4670,4671],{"id":4671},"现象",[17,4673,4674,4675,4678,4679,1213],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[198,4676,4677],{},"post.body.toc"," 上。直接用它渲染目录，发现",[21,4680,4681],{},"只有 h2 标题，h3 全部丢失",[12,4683,4684],{"id":4684},"排查",[17,4686,4687,4688,4691],{},"查了官方文档，发现 ",[198,4689,4690],{},"build.markdown.toc.depth"," 可以控制深度：",[191,4693,4695],{"className":193,"code":4694,"language":195,"meta":196,"style":196},"\u002F\u002F nuxt.config.ts\ncontent: {\n  build: {\n    markdown: {\n      toc: { depth: 3 }  \u002F\u002F 包含 h3\n    }\n  }\n}\n",[198,4696,4697,4702,4709,4716,4723,4744,4748,4752],{"__ignoreMap":196},[201,4698,4699],{"class":203,"line":204},[201,4700,4701],{"class":207},"\u002F\u002F nuxt.config.ts\n",[201,4703,4704,4707],{"class":203,"line":211},[201,4705,4706],{"class":291},"content",[201,4708,1261],{"class":228},[201,4710,4711,4714],{"class":203,"line":232},[201,4712,4713],{"class":291},"  build",[201,4715,1261],{"class":228},[201,4717,4718,4721],{"class":203,"line":245},[201,4719,4720],{"class":291},"    markdown",[201,4722,1261],{"class":228},[201,4724,4725,4728,4731,4734,4736,4738,4741],{"class":203,"line":256},[201,4726,4727],{"class":291},"      toc",[201,4729,4730],{"class":228},": { ",[201,4732,4733],{"class":291},"depth",[201,4735,301],{"class":228},[201,4737,1965],{"class":221},[201,4739,4740],{"class":228}," }  ",[201,4742,4743],{"class":207},"\u002F\u002F 包含 h3\n",[201,4745,4746],{"class":203,"line":267},[201,4747,887],{"class":228},[201,4749,4750],{"class":203,"line":273},[201,4751,4244],{"class":228},[201,4753,4754],{"class":203,"line":567},[201,4755,276],{"class":228},[17,4757,4758,4759,4762],{},"配置改了，清了缓存重启，",[21,4760,4761],{},"依然只有 h2","。这个配置似乎不起作用。",[12,4764,4765],{"id":4765},"最终方案",[17,4767,4768,4769,4772],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[198,4770,4771],{},"post.body"," 的 AST 里递归提取所有标题：",[191,4774,4776],{"className":193,"code":4775,"language":195,"meta":196,"style":196},"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",[198,4777,4778,4822,4841,4854,4859,4912,4941,4971,4996,5000,5024,5028,5035],{"__ignoreMap":196},[201,4779,4780,4783,4786,4788,4791,4794,4797,4799,4802,4804,4807,4810,4812,4815,4817,4819],{"class":203,"line":204},[201,4781,4782],{"class":214},"function",[201,4784,4785],{"class":291}," extractHeadings",[201,4787,710],{"class":228},[201,4789,4790],{"class":713},"node",[201,4792,4793],{"class":214},":",[201,4795,4796],{"class":221}," any",[201,4798,338],{"class":228},[201,4800,4801],{"class":713},"list",[201,4803,4793],{"class":214},[201,4805,4806],{"class":291}," Heading",[201,4808,4809],{"class":228},"[] ",[201,4811,775],{"class":214},[201,4813,4814],{"class":228}," [])",[201,4816,4793],{"class":214},[201,4818,4806],{"class":291},[201,4820,4821],{"class":228},"[] {\n",[201,4823,4824,4827,4829,4832,4835,4838],{"class":203,"line":211},[201,4825,4826],{"class":214},"  if",[201,4828,4028],{"class":228},[201,4830,4831],{"class":214},"!",[201,4833,4834],{"class":228},"node) ",[201,4836,4837],{"class":214},"return",[201,4839,4840],{"class":228}," list\n",[201,4842,4843,4845,4848,4851],{"class":203,"line":232},[201,4844,4826],{"class":214},[201,4846,4847],{"class":228}," (Array.",[201,4849,4850],{"class":291},"isArray",[201,4852,4853],{"class":228},"(node)) {\n",[201,4855,4856],{"class":203,"line":245},[201,4857,4858],{"class":207},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[201,4860,4861,4863,4865,4868,4871,4873,4876,4878,4881,4884,4887,4889,4892,4895,4897,4899,4901,4904,4907,4909],{"class":203,"line":256},[201,4862,2836],{"class":214},[201,4864,4028],{"class":228},[201,4866,4867],{"class":214},"typeof",[201,4869,4870],{"class":228}," node[",[201,4872,3595],{"class":221},[201,4874,4875],{"class":228},"] ",[201,4877,1398],{"class":214},[201,4879,4880],{"class":238}," 'string'",[201,4882,4883],{"class":214}," &&",[201,4885,4886],{"class":238}," \u002F",[201,4888,1312],{"class":214},[201,4890,4891],{"class":1315},"h",[201,4893,4894],{"class":221},"[1-6]",[201,4896,1387],{"class":214},[201,4898,1309],{"class":238},[201,4900,640],{"class":228},[201,4902,4903],{"class":291},"test",[201,4905,4906],{"class":228},"(node[",[201,4908,3595],{"class":221},[201,4910,4911],{"class":228},"])) {\n",[201,4913,4914,4917,4920,4922,4925,4927,4929,4932,4935,4937,4939],{"class":203,"line":267},[201,4915,4916],{"class":214},"      const",[201,4918,4919],{"class":221}," level",[201,4921,225],{"class":214},[201,4923,4924],{"class":291}," Number",[201,4926,4906],{"class":228},[201,4928,3595],{"class":221},[201,4930,4931],{"class":228},"].",[201,4933,4934],{"class":291},"charAt",[201,4936,710],{"class":228},[201,4938,3662],{"class":221},[201,4940,3901],{"class":228},[201,4942,4943,4945,4948,4950,4953,4956,4959,4961,4963,4966,4969],{"class":203,"line":273},[201,4944,4916],{"class":214},[201,4946,4947],{"class":221}," text",[201,4949,225],{"class":214},[201,4951,4952],{"class":291}," extractText",[201,4954,4955],{"class":228},"(node.",[201,4957,4958],{"class":291},"slice",[201,4960,710],{"class":228},[201,4962,1979],{"class":221},[201,4964,4965],{"class":228},")).",[201,4967,4968],{"class":291},"trim",[201,4970,943],{"class":228},[201,4972,4973,4976,4979,4982,4984,4987,4990,4993],{"class":203,"line":567},[201,4974,4975],{"class":228},"      list.",[201,4977,4978],{"class":291},"push",[201,4980,4981],{"class":228},"({ id: node[",[201,4983,3662],{"class":221},[201,4985,4986],{"class":228},"]?.id ",[201,4988,4989],{"class":214},"||",[201,4991,4992],{"class":291}," slugify",[201,4994,4995],{"class":228},"(text), text, level })\n",[201,4997,4998],{"class":203,"line":577},[201,4999,887],{"class":228},[201,5001,5002,5005,5007,5009,5012,5015,5018,5021],{"class":203,"line":587},[201,5003,5004],{"class":214},"    for",[201,5006,4028],{"class":228},[201,5008,3790],{"class":214},[201,5010,5011],{"class":221}," child",[201,5013,5014],{"class":214}," of",[201,5016,5017],{"class":228}," node) ",[201,5019,5020],{"class":291},"extractHeadings",[201,5022,5023],{"class":228},"(child, list)\n",[201,5025,5026],{"class":203,"line":597},[201,5027,4244],{"class":228},[201,5029,5030,5033],{"class":203,"line":602},[201,5031,5032],{"class":214},"  return",[201,5034,4840],{"class":228},[201,5036,5037],{"class":203,"line":612},[201,5038,276],{"class":228},[17,5040,5041,5042,5044,5045,5048],{},"关键点：Nuxt Content v3 的 body 使用 ",[21,5043,9],{}," 格式，标题节点是数组 ",[198,5046,5047],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[4657,5050,5052],{"id":5051},"问题二图片路径解析错误","问题二：图片路径解析错误",[12,5054,4671],{"id":5055},"现象-1",[17,5057,5058],{},"markdown 里用相对路径引用图片：",[191,5060,5064],{"className":5061,"code":5062,"language":5063,"meta":196,"style":196},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[198,5065,5066],{"__ignoreMap":196},[201,5067,5068,5071,5075,5078,5082],{"class":203,"line":204},[201,5069,5070],{"class":228},"![",[201,5072,5074],{"class":5073},"smiIW","SSHFS挂载效果",[201,5076,5077],{"class":228},"](",[201,5079,5081],{"class":5080},"szbl0","img\u002Fsshfs.avif",[201,5083,615],{"class":228},[17,5085,5086,5087,5090,5091,5094,5095,1213],{},"HTML 输出的 ",[198,5088,5089],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[198,5092,5093],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[198,5096,5097],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[12,5099,4684],{"id":5100},"排查-1",[1473,5102,5103,5110,5113,5119],{},[124,5104,5105,5106,5109],{},"检查了 ",[198,5107,5108],{},"\u003Cbase>"," 标签 —— 没有",[124,5111,5112],{},"检查了重定向 —— 没有",[124,5114,5115,5116,5118],{},"直接访问 ",[198,5117,5097],{}," —— 返回 200 OK",[124,5120,5115,5121,5123],{},[198,5122,5093],{}," —— 404",[17,5125,5126,5127,1213],{},"服务器路由没问题，问题是",[21,5128,5129],{},"浏览器解析相对路径时基于了错误的基准 URL",[17,5131,5132],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[12,5134,4765],{"id":5135},"最终方案-1",[17,5137,5138],{},"两步解决：",[17,5140,5141],{},[21,5142,5143],{},"第一步：自定义 ProseImg 组件",[17,5145,5146,5147,5150],{},"创建 ",[198,5148,5149],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[191,5152,5154],{"className":4535,"code":5153,"language":4537,"meta":196,"style":196},"\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",[198,5155,5156,5168,5182,5202,5218,5255,5288,5312,5316,5324,5328,5337,5364],{"__ignoreMap":196},[201,5157,5158,5160,5163,5166],{"class":203,"line":204},[201,5159,4544],{"class":228},[201,5161,5162],{"class":297},"script",[201,5164,5165],{"class":291}," setup",[201,5167,4549],{"class":228},[201,5169,5170,5172,5175,5177,5180],{"class":203,"line":211},[201,5171,3790],{"class":214},[201,5173,5174],{"class":221}," route",[201,5176,225],{"class":214},[201,5178,5179],{"class":291}," useRoute",[201,5181,943],{"class":228},[201,5183,5184,5186,5189,5191,5194,5197,5200],{"class":203,"line":232},[201,5185,3790],{"class":214},[201,5187,5188],{"class":221}," resolvedSrc",[201,5190,225],{"class":214},[201,5192,5193],{"class":291}," computed",[201,5195,5196],{"class":228},"(() ",[201,5198,5199],{"class":214},"=>",[201,5201,229],{"class":228},[201,5203,5204,5206,5208,5210,5213,5215],{"class":203,"line":245},[201,5205,4826],{"class":214},[201,5207,4028],{"class":228},[201,5209,4831],{"class":214},[201,5211,5212],{"class":228},"props.src) ",[201,5214,4837],{"class":214},[201,5216,5217],{"class":238}," ''\n",[201,5219,5220,5222,5225,5228,5230,5233,5235,5237,5240,5242,5244,5247,5250,5252],{"class":203,"line":256},[201,5221,4826],{"class":214},[201,5223,5224],{"class":228}," (props.src.",[201,5226,5227],{"class":291},"startsWith",[201,5229,710],{"class":228},[201,5231,5232],{"class":238},"'\u002F'",[201,5234,3431],{"class":228},[201,5236,4989],{"class":214},[201,5238,5239],{"class":228}," props.src.",[201,5241,5227],{"class":291},[201,5243,710],{"class":228},[201,5245,5246],{"class":238},"'http'",[201,5248,5249],{"class":228},")) ",[201,5251,4837],{"class":214},[201,5253,5254],{"class":228}," props.src\n",[201,5256,5257,5260,5263,5265,5268,5270,5272,5274,5278,5280,5282,5284,5286],{"class":203,"line":267},[201,5258,5259],{"class":214},"  const",[201,5261,5262],{"class":221}," base",[201,5264,225],{"class":214},[201,5266,5267],{"class":228}," route.path.",[201,5269,1304],{"class":291},[201,5271,710],{"class":228},[201,5273,1309],{"class":238},[201,5275,5277],{"class":5276},"sCnZR","\\\u002F",[201,5279,1387],{"class":214},[201,5281,1309],{"class":238},[201,5283,338],{"class":228},[201,5285,1322],{"class":238},[201,5287,615],{"class":228},[201,5289,5290,5292,5295,5298,5301,5304,5306,5309],{"class":203,"line":273},[201,5291,5032],{"class":214},[201,5293,5294],{"class":238}," `${",[201,5296,5297],{"class":228},"base",[201,5299,5300],{"class":238},"}\u002F${",[201,5302,5303],{"class":228},"props",[201,5305,640],{"class":238},[201,5307,5308],{"class":228},"src",[201,5310,5311],{"class":238},"}`\n",[201,5313,5314],{"class":203,"line":567},[201,5315,2675],{"class":228},[201,5317,5318,5320,5322],{"class":203,"line":577},[201,5319,4589],{"class":228},[201,5321,5162],{"class":297},[201,5323,4549],{"class":228},[201,5325,5326],{"class":203,"line":587},[201,5327,527],{"emptyLinePlaceholder":394},[201,5329,5330,5332,5335],{"class":203,"line":597},[201,5331,4544],{"class":228},[201,5333,5334],{"class":297},"template",[201,5336,4549],{"class":228},[201,5338,5339,5342,5345,5348,5350,5353,5356,5358,5361],{"class":203,"line":602},[201,5340,5341],{"class":228},"  \u003C",[201,5343,5344],{"class":297},"img",[201,5346,5347],{"class":291}," :src",[201,5349,775],{"class":228},[201,5351,5352],{"class":238},"\"resolvedSrc\"",[201,5354,5355],{"class":291}," :alt",[201,5357,775],{"class":228},[201,5359,5360],{"class":238},"\"alt\"",[201,5362,5363],{"class":228}," \u002F>\n",[201,5365,5366,5368,5370],{"class":203,"line":612},[201,5367,4589],{"class":228},[201,5369,5334],{"class":297},[201,5371,4549],{"class":228},[17,5373,5374],{},[21,5375,5376],{},"第二步：启用 MDC prose 组件",[191,5378,5380],{"className":193,"code":5379,"language":195,"meta":196,"style":196},"\u002F\u002F nuxt.config.ts\nmdc: {\n  components: {\n    prose: true  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n  }\n}\n",[198,5381,5382,5386,5393,5400,5413,5417],{"__ignoreMap":196},[201,5383,5384],{"class":203,"line":204},[201,5385,4701],{"class":207},[201,5387,5388,5391],{"class":203,"line":211},[201,5389,5390],{"class":291},"mdc",[201,5392,1261],{"class":228},[201,5394,5395,5398],{"class":203,"line":232},[201,5396,5397],{"class":291},"  components",[201,5399,1261],{"class":228},[201,5401,5402,5405,5407,5410],{"class":203,"line":245},[201,5403,5404],{"class":291},"    prose",[201,5406,301],{"class":228},[201,5408,5409],{"class":221},"true",[201,5411,5412],{"class":207},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[201,5414,5415],{"class":203,"line":256},[201,5416,4244],{"class":228},[201,5418,5419],{"class":203,"line":267},[201,5420,276],{"class":228},[4657,5422,5424],{"id":5423},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[12,5426,4671],{"id":5427},"现象-2",[17,5429,5430,5431,5434],{},"原站（SvelteKit）的图片放在 ",[198,5432,5433],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[12,5436,5437],{"id":5437},"方案对比",[35,5439,5440,5453],{},[38,5441,5442],{},[41,5443,5444,5447,5450],{},[44,5445,5446],{},"方案",[44,5448,5449],{},"优点",[44,5451,5452],{},"缺点",[51,5454,5455,5466,5477],{},[41,5456,5457,5460,5463],{},[56,5458,5459],{},"复制到 public\u002F",[56,5461,5462],{},"简单",[56,5464,5465],{},"文件重复，需同步",[41,5467,5468,5471,5474],{},[56,5469,5470],{},"Vite 插件拦截",[56,5472,5473],{},"原站方案",[56,5475,5476],{},"Nuxt 不直接支持",[41,5478,5479,5482,5485],{},[56,5480,5481],{},"Server Route",[56,5483,5484],{},"不复制文件",[56,5486,5487],{},"需要手动写路由",[17,5489,5490,5491,5493],{},"最终选择 ",[21,5492,5481],{},"，和原站的 Vite 插件思路一致：",[191,5495,5497],{"className":193,"code":5496,"language":195,"meta":196,"style":196},"\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",[198,5498,5499,5504,5530,5545,5608,5622,5626,5648,5674,5692,5711,5718],{"__ignoreMap":196},[201,5500,5501],{"class":203,"line":204},[201,5502,5503],{"class":207},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[201,5505,5506,5508,5511,5514,5516,5519,5521,5524,5526,5528],{"class":203,"line":211},[201,5507,215],{"class":214},[201,5509,5510],{"class":214}," default",[201,5512,5513],{"class":291}," defineEventHandler",[201,5515,710],{"class":228},[201,5517,5518],{"class":214},"async",[201,5520,4028],{"class":228},[201,5522,5523],{"class":713},"event",[201,5525,3431],{"class":228},[201,5527,5199],{"class":214},[201,5529,229],{"class":228},[201,5531,5532,5534,5537,5539,5542],{"class":203,"line":232},[201,5533,5259],{"class":214},[201,5535,5536],{"class":221}," url",[201,5538,225],{"class":214},[201,5540,5541],{"class":291}," getRequestURL",[201,5543,5544],{"class":228},"(event)\n",[201,5546,5547,5549,5552,5554,5557,5560,5562,5564,5566,5568,5571,5573,5575,5578,5580,5583,5586,5588,5590,5592,5594,5596,5598,5600,5602,5604,5606],{"class":203,"line":245},[201,5548,5259],{"class":214},[201,5550,5551],{"class":221}," match",[201,5553,225],{"class":214},[201,5555,5556],{"class":228}," url.pathname.",[201,5558,5559],{"class":291},"match",[201,5561,710],{"class":228},[201,5563,1309],{"class":238},[201,5565,1312],{"class":214},[201,5567,5277],{"class":5276},[201,5569,5570],{"class":1315},"posts",[201,5572,5277],{"class":5276},[201,5574,710],{"class":1315},[201,5576,5577],{"class":221},"[",[201,5579,1312],{"class":214},[201,5581,5582],{"class":221},"\u002F]",[201,5584,5585],{"class":214},"+",[201,5587,1325],{"class":1315},[201,5589,5277],{"class":5276},[201,5591,5344],{"class":1315},[201,5593,5277],{"class":5276},[201,5595,710],{"class":1315},[201,5597,640],{"class":221},[201,5599,5585],{"class":214},[201,5601,1325],{"class":1315},[201,5603,1387],{"class":214},[201,5605,1309],{"class":238},[201,5607,615],{"class":228},[201,5609,5610,5612,5614,5616,5619],{"class":203,"line":256},[201,5611,4826],{"class":214},[201,5613,4028],{"class":228},[201,5615,4831],{"class":214},[201,5617,5618],{"class":228},"match) ",[201,5620,5621],{"class":214},"return\n",[201,5623,5624],{"class":203,"line":267},[201,5625,527],{"emptyLinePlaceholder":394},[201,5627,5628,5630,5633,5636,5638,5641,5643,5645],{"class":203,"line":273},[201,5629,5259],{"class":214},[201,5631,5632],{"class":228}," [, ",[201,5634,5635],{"class":221},"slug",[201,5637,338],{"class":228},[201,5639,5640],{"class":221},"filename",[201,5642,4875],{"class":228},[201,5644,775],{"class":214},[201,5646,5647],{"class":228}," match\n",[201,5649,5650,5652,5655,5657,5660,5662,5665,5668,5671],{"class":203,"line":567},[201,5651,5259],{"class":214},[201,5653,5654],{"class":221}," filePath",[201,5656,225],{"class":214},[201,5658,5659],{"class":291}," resolve",[201,5661,710],{"class":228},[201,5663,5664],{"class":238},"'content\u002Fposts'",[201,5666,5667],{"class":228},", slug, ",[201,5669,5670],{"class":238},"'img'",[201,5672,5673],{"class":228},", filename)\n",[201,5675,5676,5678,5681,5683,5686,5689],{"class":203,"line":577},[201,5677,5259],{"class":214},[201,5679,5680],{"class":221}," data",[201,5682,225],{"class":214},[201,5684,5685],{"class":214}," await",[201,5687,5688],{"class":291}," readFile",[201,5690,5691],{"class":228},"(filePath)\n",[201,5693,5694,5697,5700,5703,5705,5708],{"class":203,"line":587},[201,5695,5696],{"class":291},"  setResponseHeader",[201,5698,5699],{"class":228},"(event, ",[201,5701,5702],{"class":238},"'content-type'",[201,5704,338],{"class":228},[201,5706,5707],{"class":221},"MIME",[201,5709,5710],{"class":228},"[ext])\n",[201,5712,5713,5715],{"class":203,"line":597},[201,5714,5032],{"class":214},[201,5716,5717],{"class":228}," data\n",[201,5719,5720],{"class":203,"line":602},[201,5721,2675],{"class":228},[17,5723,5724,5725,5727,5728,5731],{},"请求 ",[198,5726,5097],{}," → 读取 ",[198,5729,5730],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[4657,5733,1465],{"id":1465},[35,5735,5736,5747],{},[38,5737,5738],{},[41,5739,5740,5742,5745],{},[44,5741,3380],{},[44,5743,5744],{},"根因",[44,5746,5446],{},[51,5748,5749,5763,5774],{},[41,5750,5751,5754,5760],{},[56,5752,5753],{},"TOC 只有 h2",[56,5755,5756,5759],{},[198,5757,5758],{},"toc.depth"," 配置不生效",[56,5761,5762],{},"从 body AST 手动提取",[41,5764,5765,5768,5771],{},[56,5766,5767],{},"图片路径错误",[56,5769,5770],{},"SPA 路由导致相对路径解析基准错误",[56,5772,5773],{},"ProseImg 组件 + mdc.prose 配置",[41,5775,5776,5779,5782],{},[56,5777,5778],{},"content 图片 404",[56,5780,5781],{},"content 目录不暴露给浏览器",[56,5783,5784],{},"Server Route 直接读取返回",[17,5786,5787],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[17,5789,5790],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[438,5792,5793],{},"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":196,"searchDepth":211,"depth":232,"links":5795},[5796,5797,5798,5799,5800,5801,5802,5803],{"id":4671,"depth":211,"text":4671},{"id":4684,"depth":211,"text":4684},{"id":4765,"depth":211,"text":4765},{"id":5055,"depth":211,"text":4671},{"id":5100,"depth":211,"text":4684},{"id":5135,"depth":211,"text":4765},{"id":5427,"depth":211,"text":4671},{"id":5437,"depth":211,"text":5437},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":4653,"description":5804},"posts\u002Fnuxt-content-lessons\u002Findex",[461,4649,5812],"Nuxt Content","18zpsOtiC1vLNeR654DuHTZR4shpuigkYAPQyuiYZmA",{"id":5815,"title":5816,"author":7,"body":5817,"description":6022,"draft":451,"extension":452,"image":6023,"meta":6024,"navigation":394,"path":6025,"pinned":451,"published":6026,"seo":6027,"stem":6028,"tags":6029,"__hash__":6031},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":9,"value":5818,"toc":6012},[5819,5823,5826,5832,5835,5839,5846,5893,5897,5903,5911,5915,5918,5921,5990,5992,6003,6009],[12,5820,5822],{"id":5821},"什么是-sveltekit","什么是 SvelteKit？",[17,5824,5825],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[17,5827,5828],{},[5344,5829],{"alt":5830,"src":5831},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[12,5833,5834],{"id":5834},"核心特点",[499,5836,5838],{"id":5837},"_1-编译时框架","1. 编译时框架",[17,5840,5841,5842,5845],{},"Svelte 和 React\u002FVue 最大的区别在于：",[21,5843,5844],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[191,5847,5849],{"className":193,"code":5848,"language":195,"meta":196,"style":196},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n",[198,5850,5851,5856,5869,5878,5889],{"__ignoreMap":196},[201,5852,5853],{"class":203,"line":204},[201,5854,5855],{"class":207},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[201,5857,5858,5861,5864,5866],{"class":203,"line":211},[201,5859,5860],{"class":214},"let",[201,5862,5863],{"class":228}," count ",[201,5865,775],{"class":214},[201,5867,5868],{"class":221}," 0\n",[201,5870,5871,5873,5876],{"class":203,"line":232},[201,5872,4782],{"class":214},[201,5874,5875],{"class":291}," increment",[201,5877,3032],{"class":228},[201,5879,5880,5883,5886],{"class":203,"line":245},[201,5881,5882],{"class":228},"  count ",[201,5884,5885],{"class":214},"+=",[201,5887,5888],{"class":221}," 1\n",[201,5890,5891],{"class":203,"line":256},[201,5892,276],{"class":228},[499,5894,5896],{"id":5895},"_2-文件路由","2. 文件路由",[17,5898,5899,5902],{},[198,5900,5901],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[191,5904,5909],{"className":5905,"code":5907,"language":5908},[5906],"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",[198,5910,5907],{"__ignoreMap":196},[499,5912,5914],{"id":5913},"_3-全栈能力","3. 全栈能力",[17,5916,5917],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[12,5919,5920],{"id":5920},"和其他框架对比",[35,5922,5923,5938],{},[38,5924,5925],{},[41,5926,5927,5930,5933,5936],{},[44,5928,5929],{},"特性",[44,5931,5932],{},"SvelteKit",[44,5934,5935],{},"Next.js",[44,5937,461],{},[51,5939,5940,5953,5966,5979],{},[41,5941,5942,5945,5948,5951],{},[56,5943,5944],{},"学习曲线",[56,5946,5947],{},"低",[56,5949,5950],{},"中",[56,5952,5950],{},[41,5954,5955,5958,5961,5964],{},[56,5956,5957],{},"包体积",[56,5959,5960],{},"小",[56,5962,5963],{},"大",[56,5965,5950],{},[41,5967,5968,5971,5974,5977],{},[56,5969,5970],{},"运行时开销",[56,5972,5973],{},"无",[56,5975,5976],{},"有",[56,5978,5976],{},[41,5980,5981,5984,5986,5988],{},[56,5982,5983],{},"生态",[56,5985,5960],{},[56,5987,5963],{},[56,5989,5963],{},[12,5991,1465],{"id":1465},[17,5993,5994,5995,5998,5999,6002],{},"如果你追求",[21,5996,5997],{},"简洁的代码","和",[21,6000,6001],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[17,6004,6005],{},[5344,6006],{"alt":6007,"src":6008},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[438,6010,6011],{},"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":196,"searchDepth":211,"depth":232,"links":6013},[6014,6015,6020,6021],{"id":5821,"depth":211,"text":5822},{"id":5834,"depth":211,"text":5834,"children":6016},[6017,6018,6019],{"id":5837,"depth":232,"text":5838},{"id":5895,"depth":232,"text":5896},{"id":5913,"depth":232,"text":5914},{"id":5920,"depth":211,"text":5920},{"id":1465,"depth":211,"text":1465},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":5816,"description":6022},"posts\u002Fsveltekit-intro\u002Findex",[5932,6030],"前端框架","CX0-JetJMg1g1czqxmpimi_Skv99hZAsMNTBHhelFAc",{"id":6033,"title":6034,"author":7,"body":6035,"description":6444,"draft":451,"extension":452,"image":6445,"meta":6446,"navigation":394,"path":6447,"pinned":451,"published":6448,"seo":6449,"stem":6450,"tags":6451,"__hash__":6455},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":9,"value":6036,"toc":6434},[6037,6040,6043,6045,6049,6052,6072,6076,6109,6113,6120,6173,6176,6394,6396,6399,6428,6431],[12,6038,6039],{"id":6039},"前言",[17,6041,6042],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[12,6044,4495],{"id":4495},[499,6046,6048],{"id":6047},"_1-安装用户管理工具","1. 安装用户管理工具",[17,6050,6051],{},"若镜像非自带用户管理：",[191,6053,6057],{"className":6054,"code":6055,"language":6056,"meta":196,"style":196},"language-dockerfile shiki shiki-themes github-light github-dark","# 权限管理\nRUN apk add --no-cache shadow\n","dockerfile",[198,6058,6059,6064],{"__ignoreMap":196},[201,6060,6061],{"class":203,"line":204},[201,6062,6063],{"class":207},"# 权限管理\n",[201,6065,6066,6069],{"class":203,"line":211},[201,6067,6068],{"class":214},"RUN",[201,6070,6071],{"class":228}," apk add --no-cache shadow\n",[499,6073,6075],{"id":6074},"_2-创建用户并修改权限","2. 创建用户并修改权限",[191,6077,6079],{"className":6054,"code":6078,"language":6056,"meta":196,"style":196},"# 创建用户组和用户\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",[198,6080,6081,6086,6093,6097,6102],{"__ignoreMap":196},[201,6082,6083],{"class":203,"line":204},[201,6084,6085],{"class":207},"# 创建用户组和用户\n",[201,6087,6088,6090],{"class":203,"line":211},[201,6089,6068],{"class":214},[201,6091,6092],{"class":228}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[201,6094,6095],{"class":203,"line":232},[201,6096,527],{"emptyLinePlaceholder":394},[201,6098,6099],{"class":203,"line":245},[201,6100,6101],{"class":207},"# 修改目录权限\n",[201,6103,6104,6106],{"class":203,"line":256},[201,6105,6068],{"class":214},[201,6107,6108],{"class":228}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[499,6110,6112],{"id":6111},"_3-指定用户启动","3. 指定用户启动",[17,6114,6115,6116,6119],{},"执行 docker 时加 ",[198,6117,6118],{},"--user"," 参数：",[191,6121,6123],{"className":357,"code":6122,"language":359,"meta":196,"style":196},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[198,6124,6125],{"__ignoreMap":196},[201,6126,6127,6130,6133,6136,6139,6142,6145,6148,6151,6153,6156,6159,6161,6163,6165,6168,6171],{"class":203,"line":204},[201,6128,6129],{"class":291},"docker",[201,6131,6132],{"class":238}," run",[201,6134,6135],{"class":221}," -it",[201,6137,6138],{"class":221}," --user",[201,6140,6141],{"class":214}," \u003C",[201,6143,6144],{"class":238},"app_use",[201,6146,6147],{"class":228},"r",[201,6149,6150],{"class":214},">",[201,6152,6141],{"class":214},[201,6154,6155],{"class":238},"app_imag",[201,6157,6158],{"class":228},"e",[201,6160,6150],{"class":214},[201,6162,4793],{"class":238},[201,6164,4544],{"class":214},[201,6166,6167],{"class":238},"image_versio",[201,6169,6170],{"class":228},"n",[201,6172,4549],{"class":214},[12,6174,6175],{"id":6175},"完整示例",[191,6177,6179],{"className":6054,"code":6178,"language":6056,"meta":196,"style":196},"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",[198,6180,6181,6189,6193,6198,6211,6215,6222,6226,6232,6237,6244,6249,6256,6260,6268,6272,6276,6283,6287,6295,6299,6306,6310,6324,6332,6336,6341,6348,6355,6359,6364,6372],{"__ignoreMap":196},[201,6182,6183,6186],{"class":203,"line":204},[201,6184,6185],{"class":214},"FROM",[201,6187,6188],{"class":228}," alpine:latest\n",[201,6190,6191],{"class":203,"line":211},[201,6192,527],{"emptyLinePlaceholder":394},[201,6194,6195],{"class":203,"line":232},[201,6196,6197],{"class":207},"# 国内源\n",[201,6199,6200,6202,6205,6208],{"class":203,"line":245},[201,6201,6068],{"class":214},[201,6203,6204],{"class":228}," sed -i ",[201,6206,6207],{"class":238},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[201,6209,6210],{"class":228}," \u002Fetc\u002Fapk\u002Frepositories\n",[201,6212,6213],{"class":203,"line":256},[201,6214,527],{"emptyLinePlaceholder":394},[201,6216,6217,6219],{"class":203,"line":267},[201,6218,6068],{"class":214},[201,6220,6221],{"class":228}," apk update --no-cache\n",[201,6223,6224],{"class":203,"line":273},[201,6225,6063],{"class":207},[201,6227,6228,6230],{"class":203,"line":567},[201,6229,6068],{"class":214},[201,6231,6071],{"class":228},[201,6233,6234],{"class":203,"line":577},[201,6235,6236],{"class":207},"# 调试\n",[201,6238,6239,6241],{"class":203,"line":587},[201,6240,6068],{"class":214},[201,6242,6243],{"class":228}," apk add --no-cache bash\n",[201,6245,6246],{"class":203,"line":597},[201,6247,6248],{"class":207},"# 设置时区\n",[201,6250,6251,6253],{"class":203,"line":602},[201,6252,6068],{"class":214},[201,6254,6255],{"class":228}," apk add --no-cache tzdata\n",[201,6257,6258],{"class":203,"line":612},[201,6259,527],{"emptyLinePlaceholder":394},[201,6261,6262,6265],{"class":203,"line":618},[201,6263,6264],{"class":214},"ENV",[201,6266,6267],{"class":228}," TZ=Asia\u002FShanghai\n",[201,6269,6270],{"class":203,"line":623},[201,6271,527],{"emptyLinePlaceholder":394},[201,6273,6274],{"class":203,"line":629},[201,6275,6085],{"class":207},[201,6277,6278,6280],{"class":203,"line":646},[201,6279,6068],{"class":214},[201,6281,6282],{"class":228}," addgroup -S glog && adduser -S -G glog glog\n",[201,6284,6285],{"class":203,"line":651},[201,6286,527],{"emptyLinePlaceholder":394},[201,6288,6289,6292],{"class":203,"line":673},[201,6290,6291],{"class":214},"COPY",[201,6293,6294],{"class":228}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[201,6296,6297],{"class":203,"line":696},[201,6298,527],{"emptyLinePlaceholder":394},[201,6300,6301,6303],{"class":203,"line":701},[201,6302,6068],{"class":214},[201,6304,6305],{"class":228}," mkdir \u002Fdata\n",[201,6307,6308],{"class":203,"line":730},[201,6309,527],{"emptyLinePlaceholder":394},[201,6311,6312,6315,6318,6321],{"class":203,"line":752},[201,6313,6314],{"class":214},"VOLUME",[201,6316,6317],{"class":228}," [ ",[201,6319,6320],{"class":238},"\"\u002Fdata\"",[201,6322,6323],{"class":228}," ]\n",[201,6325,6326,6329],{"class":203,"line":769},[201,6327,6328],{"class":214},"WORKDIR",[201,6330,6331],{"class":228}," \u002Fdata\n",[201,6333,6334],{"class":203,"line":781},[201,6335,527],{"emptyLinePlaceholder":394},[201,6337,6338],{"class":203,"line":787},[201,6339,6340],{"class":207},"# 修改权限\n",[201,6342,6343,6345],{"class":203,"line":792},[201,6344,6068],{"class":214},[201,6346,6347],{"class":228}," chown -R glog:glog \u002Fdata\n",[201,6349,6350,6352],{"class":203,"line":808},[201,6351,6068],{"class":214},[201,6353,6354],{"class":228}," chown -R glog:glog \u002Fapp\n",[201,6356,6357],{"class":203,"line":824},[201,6358,527],{"emptyLinePlaceholder":394},[201,6360,6361],{"class":203,"line":830},[201,6362,6363],{"class":207},"# 指定用户启动\n",[201,6365,6366,6369],{"class":203,"line":850},[201,6367,6368],{"class":214},"USER",[201,6370,6371],{"class":228}," glog\n",[201,6373,6374,6377,6379,6382,6384,6387,6389,6392],{"class":203,"line":855},[201,6375,6376],{"class":214},"CMD",[201,6378,6317],{"class":228},[201,6380,6381],{"class":238},"\"\u002Fapp\u002Fglog_static_musl_1\"",[201,6383,338],{"class":228},[201,6385,6386],{"class":238},"\"logsweb\"",[201,6388,338],{"class":228},[201,6390,6391],{"class":238},"\".\u002Fconf.ini\"",[201,6393,6323],{"class":228},[12,6395,1465],{"id":1465},[17,6397,6398],{},"关键步骤：",[1473,6400,6401,6407,6416,6422],{},[124,6402,6403,6406],{},[198,6404,6405],{},"apk add --no-cache shadow"," - 安装用户管理工具",[124,6408,6409,4279,6412,6415],{},[198,6410,6411],{},"addgroup",[198,6413,6414],{},"adduser"," - 创建用户",[124,6417,6418,6421],{},[198,6419,6420],{},"chown -R"," - 修改目录权限",[124,6423,6424,6427],{},[198,6425,6426],{},"USER \u003Cusername>"," - 指定运行用户",[17,6429,6430],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[438,6432,6433],{},"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":196,"searchDepth":211,"depth":232,"links":6435},[6436,6437,6442,6443],{"id":6039,"depth":211,"text":6039},{"id":4495,"depth":211,"text":4495,"children":6438},[6439,6440,6441],{"id":6047,"depth":232,"text":6048},{"id":6074,"depth":232,"text":6075},{"id":6111,"depth":232,"text":6112},{"id":6175,"depth":211,"text":6175},{"id":1465,"depth":211,"text":1465},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":6034,"description":6444},"posts\u002Fdocker-user-start\u002Findex",[6452,6453,6454],"Docker","运维","Linux","dmdN3xMJ1ot--Vvs0h6h9egmqMb5VzGZ3-KkAEBbV04",{"id":6457,"title":6458,"author":7,"body":6459,"description":7215,"draft":451,"extension":452,"image":7216,"meta":7217,"navigation":394,"path":7218,"pinned":451,"published":7219,"seo":7220,"stem":7221,"tags":7222,"__hash__":7223},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":9,"value":6460,"toc":7204},[6461,6464,6467,6470,6681,6684,6725,6727,6745,6748,6886,6889,7022,7025,7028,7086,7089,7201],[12,6462,6463],{"id":6463},"解决问题",[17,6465,6466],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[12,6468,6469],{"id":6469},"查阅资料",[191,6471,6473],{"className":357,"code":6472,"language":359,"meta":196,"style":196},"# 启动容器并挂载当前目录到 `\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",[198,6474,6475,6480,6514,6518,6523,6550,6565,6579,6584,6596,6616,6624,6628,6633,6643,6653,6657,6665,6669],{"__ignoreMap":196},[201,6476,6477],{"class":203,"line":204},[201,6478,6479],{"class":207},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[201,6481,6482,6484,6486,6488,6491,6494,6496,6499,6502,6505,6508,6511],{"class":203,"line":211},[201,6483,6129],{"class":291},[201,6485,6132],{"class":238},[201,6487,6135],{"class":221},[201,6489,6490],{"class":221}," --rm",[201,6492,6493],{"class":221}," -v",[201,6495,913],{"class":238},[201,6497,6498],{"class":228},"$PWD",[201,6500,6501],{"class":238},"\":\u002Fapp",[201,6503,6504],{"class":221}," -w",[201,6506,6507],{"class":238}," \u002Fapp",[201,6509,6510],{"class":238}," ubuntu:20.04",[201,6512,6513],{"class":238}," bash\n",[201,6515,6516],{"class":203,"line":232},[201,6517,527],{"emptyLinePlaceholder":394},[201,6519,6520],{"class":203,"line":245},[201,6521,6522],{"class":207},"# 在容器内执行以下操作：\n",[201,6524,6525,6528,6531,6534,6536,6538,6541,6544,6547],{"class":203,"line":256},[201,6526,6527],{"class":291},"apt-get",[201,6529,6530],{"class":238}," update",[201,6532,6533],{"class":228}," && ",[201,6535,6527],{"class":291},[201,6537,1055],{"class":238},[201,6539,6540],{"class":221}," -y",[201,6542,6543],{"class":238}," musl-tools",[201,6545,6546],{"class":238}," golang-go",[201,6548,6549],{"class":207},"  # 安装 musl 和 Go\n",[201,6551,6552,6554,6557,6559,6562],{"class":203,"line":267},[201,6553,215],{"class":214},[201,6555,6556],{"class":228}," CC",[201,6558,775],{"class":214},[201,6560,6561],{"class":228},"musl-gcc  ",[201,6563,6564],{"class":207},"# 指定 musl 编译器\n",[201,6566,6567,6569,6572,6574,6576],{"class":203,"line":273},[201,6568,215],{"class":214},[201,6570,6571],{"class":228}," CGO_ENABLED",[201,6573,775],{"class":214},[201,6575,3662],{"class":221},[201,6577,6578],{"class":207},"  # 启用 CGO\n",[201,6580,6581],{"class":203,"line":567},[201,6582,6583],{"class":207},"# 设置代理\n",[201,6585,6586,6588,6591,6593],{"class":203,"line":577},[201,6587,507],{"class":291},[201,6589,6590],{"class":238}," env",[201,6592,6504],{"class":221},[201,6594,6595],{"class":238}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[201,6597,6598,6600,6602,6605,6608,6610,6613],{"class":203,"line":587},[201,6599,507],{"class":291},[201,6601,1125],{"class":238},[201,6603,6604],{"class":221}," -ldflags",[201,6606,6607],{"class":238}," '-linkmode external -extldflags \"-static\"'",[201,6609,1128],{"class":221},[201,6611,6612],{"class":238}," myapp",[201,6614,6615],{"class":207},"  # 静态编译\n",[201,6617,6618,6621],{"class":203,"line":597},[201,6619,6620],{"class":221},"exit",[201,6622,6623],{"class":207},"  # 退出容器\n",[201,6625,6626],{"class":203,"line":602},[201,6627,527],{"emptyLinePlaceholder":394},[201,6629,6630],{"class":203,"line":612},[201,6631,6632],{"class":207},"# 验证结果（在宿主机执行）\n",[201,6634,6635,6638,6640],{"class":203,"line":618},[201,6636,6637],{"class":291},"file",[201,6639,6612],{"class":238},[201,6641,6642],{"class":207},"  # 应显示 \"statically linked\"\n",[201,6644,6645,6648,6650],{"class":203,"line":623},[201,6646,6647],{"class":291},"ldd",[201,6649,6612],{"class":238},[201,6651,6652],{"class":207},"   # 应显示 \"not a dynamic executable\"\n",[201,6654,6655],{"class":203,"line":629},[201,6656,527],{"emptyLinePlaceholder":394},[201,6658,6659,6662],{"class":203,"line":646},[201,6660,6661],{"class":291},"wget",[201,6663,6664],{"class":238}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[201,6666,6667],{"class":203,"line":651},[201,6668,527],{"emptyLinePlaceholder":394},[201,6670,6671,6673,6676,6679],{"class":203,"line":673},[201,6672,6264],{"class":291},[201,6674,6675],{"class":238}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[201,6677,6678],{"class":228},"$PATH",[201,6680,546],{"class":238},[499,6682,6683],{"id":6683},"遇到问题",[1473,6685,6686,6711],{},[124,6687,6688,6691,6692,6695,6698,6699,6702,6704,6705,6707],{},[198,6689,6690],{},"go mod tidy"," 时报错 ",[198,6693,6694],{},"tls: failed to verify certificate: x509: certificate",[6696,6697],"br",{},"解决：",[198,6700,6701],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[6696,6703],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[6696,6706],{},[5344,6708],{"alt":6709,"src":6710},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[124,6712,6713,6714,6717,6718,6720,6721,6724],{},"直接安装 ",[198,6715,6716],{},"golang-go"," 其版本过低",[6696,6719],{},"解决: ",[198,6722,6723],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[12,6726,4765],{"id":4765},[1473,6728,6729,6736,6739],{},[124,6730,6731,6732,6735],{},"下载 ",[198,6733,6734],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[124,6737,6738],{},"编写 Dockerfile",[124,6740,6741,6742],{},"执行 ",[198,6743,6744],{},"docker build -t go-build:v1 .",[17,6746,6747],{},"Dockerfile 内容：",[191,6749,6751],{"className":6054,"code":6750,"language":6056,"meta":196,"style":196},"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",[198,6752,6753,6760,6764,6775,6779,6786,6790,6800,6804,6811,6815,6820,6827,6831,6838,6843,6850,6854,6860,6864,6870,6874],{"__ignoreMap":196},[201,6754,6755,6757],{"class":203,"line":204},[201,6756,6185],{"class":214},[201,6758,6759],{"class":228}," ubuntu:20.04\n",[201,6761,6762],{"class":203,"line":211},[201,6763,527],{"emptyLinePlaceholder":394},[201,6765,6766,6769,6772],{"class":203,"line":232},[201,6767,6768],{"class":214},"LABEL",[201,6770,6771],{"class":228}," org.opencontainers.image.authors=",[201,6773,6774],{"class":238},"\"sutong\"\n",[201,6776,6777],{"class":203,"line":245},[201,6778,527],{"emptyLinePlaceholder":394},[201,6780,6781,6783],{"class":203,"line":256},[201,6782,6291],{"class":214},[201,6784,6785],{"class":228}," go\u002F \u002Fgo\u002F\n",[201,6787,6788],{"class":203,"line":267},[201,6789,527],{"emptyLinePlaceholder":394},[201,6791,6792,6794,6797],{"class":203,"line":273},[201,6793,6264],{"class":214},[201,6795,6796],{"class":228}," PATH=",[201,6798,6799],{"class":238},"\"\u002Fgo\u002Fbin:$PATH\"\n",[201,6801,6802],{"class":203,"line":567},[201,6803,527],{"emptyLinePlaceholder":394},[201,6805,6806,6808],{"class":203,"line":577},[201,6807,6068],{"class":214},[201,6809,6810],{"class":228}," apt-get update && apt-get install -y musl-tools\n",[201,6812,6813],{"class":203,"line":587},[201,6814,527],{"emptyLinePlaceholder":394},[201,6816,6817],{"class":203,"line":597},[201,6818,6819],{"class":207},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[201,6821,6822,6824],{"class":203,"line":602},[201,6823,6068],{"class":214},[201,6825,6826],{"class":228}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[201,6828,6829],{"class":203,"line":612},[201,6830,6564],{"class":207},[201,6832,6833,6835],{"class":203,"line":618},[201,6834,6264],{"class":214},[201,6836,6837],{"class":228}," CC=musl-gcc\n",[201,6839,6840],{"class":203,"line":623},[201,6841,6842],{"class":207},"# 启用 CGO\n",[201,6844,6845,6847],{"class":203,"line":629},[201,6846,6264],{"class":214},[201,6848,6849],{"class":228}," CGO_ENABLED=1\n",[201,6851,6852],{"class":203,"line":646},[201,6853,527],{"emptyLinePlaceholder":394},[201,6855,6856,6858],{"class":203,"line":651},[201,6857,6314],{"class":214},[201,6859,6331],{"class":228},[201,6861,6862],{"class":203,"line":673},[201,6863,527],{"emptyLinePlaceholder":394},[201,6865,6866,6868],{"class":203,"line":696},[201,6867,6328],{"class":214},[201,6869,6331],{"class":228},[201,6871,6872],{"class":203,"line":701},[201,6873,527],{"emptyLinePlaceholder":394},[201,6875,6876,6878,6881,6884],{"class":203,"line":730},[201,6877,6376],{"class":214},[201,6879,6880],{"class":228}," [",[201,6882,6883],{"class":238},"\"\u002Fbin\u002Fbash\"",[201,6885,344],{"class":228},[499,6887,6888],{"id":6888},"构建",[191,6890,6892],{"className":357,"code":6891,"language":359,"meta":196,"style":196},"# 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",[198,6893,6894,6899,6903,6908,6934,6938,6943,6976,6981,6985,7017],{"__ignoreMap":196},[201,6895,6896],{"class":203,"line":204},[201,6897,6898],{"class":207},"# docker build -t go-build:v1 .\n",[201,6900,6901],{"class":203,"line":211},[201,6902,527],{"emptyLinePlaceholder":394},[201,6904,6905],{"class":203,"line":232},[201,6906,6907],{"class":207},"# 直接执行编译 编译后删除\n",[201,6909,6910,6912,6914,6916,6918,6921,6924,6927,6929,6931],{"class":203,"line":245},[201,6911,6129],{"class":291},[201,6913,6132],{"class":238},[201,6915,6490],{"class":221},[201,6917,6493],{"class":221},[201,6919,6920],{"class":238}," .:\u002Fdata",[201,6922,6923],{"class":238}," go-build:v1",[201,6925,6926],{"class":238}," go",[201,6928,1125],{"class":238},[201,6930,1128],{"class":221},[201,6932,6933],{"class":238}," app_musl\n",[201,6935,6936],{"class":203,"line":256},[201,6937,527],{"emptyLinePlaceholder":394},[201,6939,6940],{"class":203,"line":267},[201,6941,6942],{"class":207},"# 构建编译环境\n",[201,6944,6945,6947,6949,6951,6954,6956,6958,6961,6963,6966,6969,6971,6973],{"class":203,"line":273},[201,6946,6129],{"class":291},[201,6948,6132],{"class":238},[201,6950,6135],{"class":221},[201,6952,6953],{"class":221}," -d",[201,6955,6493],{"class":221},[201,6957,6920],{"class":238},[201,6959,6960],{"class":221}," --name",[201,6962,6141],{"class":214},[201,6964,6965],{"class":238},"容器名",[201,6967,6968],{"class":228},"称",[201,6970,6150],{"class":214},[201,6972,6923],{"class":238},[201,6974,6975],{"class":238}," \u002Fbin\u002Fbash\n",[201,6977,6978],{"class":203,"line":567},[201,6979,6980],{"class":207},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[201,6982,6983],{"class":203,"line":577},[201,6984,527],{"emptyLinePlaceholder":394},[201,6986,6987,6989,6992,6994,6996,6998,7000,7002,7004,7006,7008,7010,7013,7015],{"class":203,"line":587},[201,6988,6129],{"class":291},[201,6990,6991],{"class":238}," exec",[201,6993,6135],{"class":221},[201,6995,6141],{"class":214},[201,6997,6965],{"class":238},[201,6999,6968],{"class":228},[201,7001,6150],{"class":214},[201,7003,6926],{"class":238},[201,7005,1125],{"class":238},[201,7007,1128],{"class":221},[201,7009,6141],{"class":214},[201,7011,7012],{"class":238},"myap",[201,7014,17],{"class":228},[201,7016,4549],{"class":214},[201,7018,7019],{"class":203,"line":597},[201,7020,7021],{"class":207},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[499,7023,7024],{"id":7024},"额外注意",[17,7026,7027],{},"若没科学上网需要设置代理：",[191,7029,7031],{"className":357,"code":7030,"language":359,"meta":196,"style":196},"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",[198,7032,7033,7051,7061,7077],{"__ignoreMap":196},[201,7034,7035,7037,7039,7041,7043,7045,7047,7049],{"class":203,"line":204},[201,7036,6129],{"class":291},[201,7038,6991],{"class":238},[201,7040,6135],{"class":221},[201,7042,6141],{"class":214},[201,7044,6965],{"class":238},[201,7046,6968],{"class":228},[201,7048,6150],{"class":214},[201,7050,6975],{"class":238},[201,7052,7053,7055,7057,7059],{"class":203,"line":211},[201,7054,507],{"class":291},[201,7056,6590],{"class":238},[201,7058,6504],{"class":221},[201,7060,6595],{"class":238},[201,7062,7063,7065,7067,7069,7071,7073,7075],{"class":203,"line":232},[201,7064,507],{"class":291},[201,7066,1125],{"class":238},[201,7068,6604],{"class":221},[201,7070,6607],{"class":238},[201,7072,1128],{"class":221},[201,7074,6612],{"class":238},[201,7076,6615],{"class":207},[201,7078,7079,7081,7083],{"class":203,"line":245},[201,7080,507],{"class":291},[201,7082,1125],{"class":238},[201,7084,7085],{"class":207}," # musl 库编译\n",[499,7087,7088],{"id":7088},"运行环境示例",[191,7090,7092],{"className":6054,"code":7091,"language":6056,"meta":196,"style":196},"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",[198,7093,7094,7100,7104,7112,7116,7123,7127,7134,7141,7145,7152,7156,7163,7167,7187,7191,7196],{"__ignoreMap":196},[201,7095,7096,7098],{"class":203,"line":204},[201,7097,6185],{"class":214},[201,7099,6188],{"class":228},[201,7101,7102],{"class":203,"line":211},[201,7103,527],{"emptyLinePlaceholder":394},[201,7105,7106,7108,7110],{"class":203,"line":232},[201,7107,6768],{"class":214},[201,7109,6771],{"class":228},[201,7111,6774],{"class":238},[201,7113,7114],{"class":203,"line":245},[201,7115,527],{"emptyLinePlaceholder":394},[201,7117,7118,7120],{"class":203,"line":256},[201,7119,6291],{"class":214},[201,7121,7122],{"class":228}," glog_musl \u002Fapp\u002F\n",[201,7124,7125],{"class":203,"line":267},[201,7126,527],{"emptyLinePlaceholder":394},[201,7128,7129,7131],{"class":203,"line":273},[201,7130,6291],{"class":214},[201,7132,7133],{"class":228}," conf.ini \u002Fdata\u002F\n",[201,7135,7136,7138],{"class":203,"line":567},[201,7137,6291],{"class":214},[201,7139,7140],{"class":228}," ipname.ini \u002Fdata\u002F\n",[201,7142,7143],{"class":203,"line":577},[201,7144,527],{"emptyLinePlaceholder":394},[201,7146,7147,7149],{"class":203,"line":587},[201,7148,6328],{"class":214},[201,7150,7151],{"class":228}," \u002Fapp\n",[201,7153,7154],{"class":203,"line":597},[201,7155,527],{"emptyLinePlaceholder":394},[201,7157,7158,7160],{"class":203,"line":602},[201,7159,6068],{"class":214},[201,7161,7162],{"class":228}," chmod 777 \u002Fapp\u002Fglog_musl\n",[201,7164,7165],{"class":203,"line":612},[201,7166,527],{"emptyLinePlaceholder":394},[201,7168,7169,7171,7173,7176,7178,7180,7182,7185],{"class":203,"line":618},[201,7170,6376],{"class":214},[201,7172,6880],{"class":228},[201,7174,7175],{"class":238},"\".\u002Fglog_musl\"",[201,7177,338],{"class":228},[201,7179,6386],{"class":238},[201,7181,338],{"class":228},[201,7183,7184],{"class":238},"\"\u002Fdata\u002Fconf.ini\"",[201,7186,344],{"class":228},[201,7188,7189],{"class":203,"line":623},[201,7190,527],{"emptyLinePlaceholder":394},[201,7192,7193],{"class":203,"line":629},[201,7194,7195],{"class":207},"# docker build -t glog:v1 .\n",[201,7197,7198],{"class":203,"line":646},[201,7199,7200],{"class":207},"# docker run -p 6801:6801 glog:v1\n",[438,7202,7203],{},"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":196,"searchDepth":211,"depth":232,"links":7205},[7206,7207,7210],{"id":6463,"depth":211,"text":6463},{"id":6469,"depth":211,"text":6469,"children":7208},[7209],{"id":6683,"depth":232,"text":6683},{"id":4765,"depth":211,"text":4765,"children":7211},[7212,7213,7214],{"id":6888,"depth":232,"text":6888},{"id":7024,"depth":232,"text":7024},{"id":7088,"depth":232,"text":7088},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":6458,"description":7215},"posts\u002Fdocker-musl-build\u002Findex",[6452,1516,6453],"dkLU2pJYx10M_f4jWmgiVUow7XSYd6S5JavgEqrFrvI",1780733791859]