[{"data":1,"prerenderedAt":13169},["ShallowReactive",2],{"posts-list":3},[4,466,2132,3418,4606,5170,6598,7557,8330,10348,10628,11772,11985,12404],{"id":5,"title":6,"author":7,"body":8,"description":450,"difficulty":451,"draft":452,"extension":453,"image":454,"meta":455,"navigation":394,"path":456,"pinned":394,"published":457,"seo":458,"series":459,"seriesOrder":204,"stem":460,"tags":461,"updated":464,"__hash__":465},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎使用 SuBlog",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",{},"SuBlog","，这是站点的第一篇示例文章。",[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\u002Fexample.com\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\u002Fexample.com\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 .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":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},"Nuxt 博客开发","posts\u002Fhello-world\u002Findex",[462,463],"博客","Nuxt","2026-05-31","f9MCboCBfQIUxT8aKv6aKGDFxysiG8ZG0YXr5X5LTFo",{"id":467,"title":468,"author":7,"body":469,"description":2116,"difficulty":2117,"draft":452,"extension":453,"image":2118,"meta":2119,"navigation":394,"path":2120,"pinned":452,"published":2121,"seo":2122,"series":2123,"seriesOrder":232,"stem":2124,"tags":2125,"updated":2130,"__hash__":2131},"posts\u002Fposts\u002Fgo-embed-react-single-binary\u002Findex.md","Go 单文件打包进阶：React 前端 + 交叉编译，一次踩完所有坑",{"type":9,"value":470,"toc":2095},[471,474,486,498,505,508,546,549,560,563,568,579,628,674,692,696,717,751,755,1107,1110,1149,1153,1515,1518,1522,1729,1740,1743,1754,1769,1782,1793,1797,1804,1821,1835,1841,1845,1863,1868,1913,1927,1938,1942,1957,1966,1988,1997,2001,2011,2014,2043,2052,2055,2058,2085,2092],[12,472,473],{"id":473},"背景",[17,475,476,477,481,482,485],{},"这是「Go 单文件打包」系列第三篇。上一篇（",[422,478,480],{"href":479},"\u002Fposts\u002Fgo-embed-frontend","把前端塞进二进制","）记录了一个 Gin + Vue 3 项目的 embed 方案。这次换了个项目继续踩坑：",[21,483,484],{},"Magic Img","——一个自托管、本地优先的 AI 生图提示词管理工具，技术栈是：",[121,487,488,491],{},[124,489,490],{},"前端：React 19 + TypeScript + Vite + Tailwind v4（UI 库 Magic UI + shadcn\u002Fui）",[124,492,493,494,497],{},"后端：Go + Gin + GORM + SQLite（",[21,495,496],{},"modernc 纯 Go 驱动，无 cgo","）",[17,499,500,501,504],{},"目标一样：",[21,502,503],{},"一个二进制拖到任何机器上就能跑","——前端、API、数据库全部内嵌。",[17,506,507],{},"和上篇不同的地方有三个，正好是这篇要讲的重点：",[509,510,511,525,539],"ol",{},[124,512,513,514,520,521,524],{},"前端用了 ",[21,515,516,517],{},"HashRouter + Vite base ",[198,518,519],{},"\u002Fdist","，服务端",[21,522,523],{},"不再需要 SPA fallback","（上篇坑一的新解法）；",[124,526,527,528,534,535,538],{},"embed 用 ",[21,529,530,533],{},[198,531,532],{},"all:"," 前缀","一劳永逸解决 ",[198,536,537],{},"_"," 开头文件被排除的问题（上篇坑二的升级解法）；",[124,540,541,542,545],{},"需要",[21,543,544],{},"在 Windows 上交叉编译 Linux 产物","，踩了「生成的文件无法在 Linux 执行」的坑。",[12,547,548],{"id":548},"核心思路",[17,550,551,552,555,556,559],{},"和上篇一致：",[198,553,554],{},"\u002F\u002Fgo:embed"," 在编译期把前端 ",[198,557,558],{},"dist\u002F"," 塞进二进制，运行时由 Go 直接提供静态服务。差异在于路由策略和缓存策略，后面细说。",[12,561,562],{"id":562},"逐步实现",[564,565,567],"h3",{"id":566},"_1-前端hash-路由-base-dist","1. 前端：hash 路由 + base \u002Fdist",[17,569,570,571,574,575,578],{},"vite.config.ts 里固定 ",[198,572,573],{},"base: \"\u002Fdist\u002F\"","，路由用 ",[198,576,577],{},"HashRouter","：",[191,580,584],{"className":581,"code":582,"language":583,"meta":196,"style":196},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F vite.config.ts\nexport default defineConfig({\n  base: \"\u002Fdist\u002F\",   \u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n  ...\n})\n","ts",[198,585,586,591,604,618,623],{"__ignoreMap":196},[201,587,588],{"class":203,"line":204},[201,589,590],{"class":207},"\u002F\u002F vite.config.ts\n",[201,592,593,595,598,601],{"class":203,"line":211},[201,594,215],{"class":214},[201,596,597],{"class":214}," default",[201,599,600],{"class":291}," defineConfig",[201,602,603],{"class":228},"({\n",[201,605,606,609,612,615],{"class":203,"line":232},[201,607,608],{"class":228},"  base: ",[201,610,611],{"class":238},"\"\u002Fdist\u002F\"",[201,613,614],{"class":228},",   ",[201,616,617],{"class":207},"\u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n",[201,619,620],{"class":203,"line":245},[201,621,622],{"class":214},"  ...\n",[201,624,625],{"class":203,"line":256},[201,626,627],{"class":228},"})\n",[191,629,633],{"className":630,"code":631,"language":632,"meta":196,"style":196},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n\u003CHashRouter>\n  \u003CRoutes>...\u003C\u002FRoutes>\n\u003C\u002FHashRouter>\n","tsx",[198,634,635,640,650,665],{"__ignoreMap":196},[201,636,637],{"class":203,"line":204},[201,638,639],{"class":207},"\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n",[201,641,642,645,647],{"class":203,"line":211},[201,643,644],{"class":228},"\u003C",[201,646,577],{"class":221},[201,648,649],{"class":228},">\n",[201,651,652,655,658,661,663],{"class":203,"line":232},[201,653,654],{"class":228},"  \u003C",[201,656,657],{"class":221},"Routes",[201,659,660],{"class":228},">...\u003C\u002F",[201,662,657],{"class":221},[201,664,649],{"class":228},[201,666,667,670,672],{"class":203,"line":245},[201,668,669],{"class":228},"\u003C\u002F",[201,671,577],{"class":221},[201,673,649],{"class":228},[17,675,676,679,680,683,684,687,688,691],{},[21,677,678],{},"为什么这样选","：history 路由需要服务端把未知路径全部 fallback 回 ",[198,681,682],{},"index.html","（上篇坑一）。hash 路由下浏览器请求的永远是 ",[198,685,686],{},"\u002Fdist\u002F"," 这一个入口，",[21,689,690],{},"静态服务直接返回 index.html 就完事，一个 fallback 都不用写","。内嵌场景里这是最省心的组合。",[564,693,695],{"id":694},"_2-构建产物复制进后端目录","2. 构建产物复制进后端目录",[17,697,698,701,702,704,705,708,709,712,713,716],{},[198,699,700],{},"npm run build"," 除了编译，还会把 ",[198,703,558],{}," 复制到 ",[198,706,707],{},"backend\u002Fweb\u002Fdist","（一个小脚本 ",[198,710,711],{},"scripts\u002Fcopy-dist.mjs"," 干这个活），供 ",[198,714,715],{},"go:embed"," 使用：",[191,718,722],{"className":719,"code":720,"language":721,"meta":196,"style":196},"language-json shiki shiki-themes github-light github-dark","\u002F\u002F frontend\u002Fpackage.json\n\"scripts\": {\n  \"build\": \"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n}\n","json",[198,723,724,729,737,747],{"__ignoreMap":196},[201,725,726],{"class":203,"line":204},[201,727,728],{"class":207},"\u002F\u002F frontend\u002Fpackage.json\n",[201,730,731,734],{"class":203,"line":211},[201,732,733],{"class":238},"\"scripts\"",[201,735,736],{"class":228},": {\n",[201,738,739,742,744],{"class":203,"line":232},[201,740,741],{"class":221},"  \"build\"",[201,743,301],{"class":228},[201,745,746],{"class":238},"\"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n",[201,748,749],{"class":203,"line":245},[201,750,276],{"class":228},[564,752,754],{"id":753},"_3-后端embed-httpfilesystem-包装","3. 后端：embed + http.FileSystem 包装",[191,756,760],{"className":757,"code":758,"language":759,"meta":196,"style":196},"language-go shiki shiki-themes github-light github-dark","\u002F\u002F backend\u002Fweb\u002Fembed.go\npackage web\n\nimport (\n    \"embed\"\n    \"net\u002Fhttp\"\n    \"strings\"\n)\n\n\u002F\u002Fgo:embed all:dist\nvar distFS embed.FS\n\n\u002F\u002F Dist 内嵌前端静态文件系统\nvar Dist http.FileSystem = &fileSystem{prefix: \"dist\", fs: &distFS}\n\ntype fileSystem struct {\n    prefix string\n    fs     *embed.FS\n}\n\nfunc (f *fileSystem) Open(name string) (http.File, error) {\n    if name == \"\u002F\" {\n        name = f.prefix\n    } else {\n        name = f.prefix + \"\u002F\" + strings.TrimPrefix(name, \"\u002F\")\n    }\n    return http.FS(f.fs).Open(name)\n}\n","go",[198,761,762,767,775,780,788,799,808,817,823,828,834,851,856,862,901,906,920,929,944,949,954,1004,1021,1033,1044,1076,1082,1102],{"__ignoreMap":196},[201,763,764],{"class":203,"line":204},[201,765,766],{"class":207},"\u002F\u002F backend\u002Fweb\u002Fembed.go\n",[201,768,769,772],{"class":203,"line":211},[201,770,771],{"class":214},"package",[201,773,774],{"class":291}," web\n",[201,776,777],{"class":203,"line":232},[201,778,779],{"emptyLinePlaceholder":394},"\n",[201,781,782,785],{"class":203,"line":245},[201,783,784],{"class":214},"import",[201,786,787],{"class":228}," (\n",[201,789,790,793,796],{"class":203,"line":256},[201,791,792],{"class":238},"    \"",[201,794,795],{"class":291},"embed",[201,797,798],{"class":238},"\"\n",[201,800,801,803,806],{"class":203,"line":267},[201,802,792],{"class":238},[201,804,805],{"class":291},"net\u002Fhttp",[201,807,798],{"class":238},[201,809,810,812,815],{"class":203,"line":273},[201,811,792],{"class":238},[201,813,814],{"class":291},"strings",[201,816,798],{"class":238},[201,818,820],{"class":203,"line":819},8,[201,821,822],{"class":228},")\n",[201,824,826],{"class":203,"line":825},9,[201,827,779],{"emptyLinePlaceholder":394},[201,829,831],{"class":203,"line":830},10,[201,832,833],{"class":207},"\u002F\u002Fgo:embed all:dist\n",[201,835,837,840,843,845,848],{"class":203,"line":836},11,[201,838,839],{"class":214},"var",[201,841,842],{"class":228}," distFS ",[201,844,795],{"class":291},[201,846,847],{"class":228},".",[201,849,850],{"class":291},"FS\n",[201,852,854],{"class":203,"line":853},12,[201,855,779],{"emptyLinePlaceholder":394},[201,857,859],{"class":203,"line":858},13,[201,860,861],{"class":207},"\u002F\u002F Dist 内嵌前端静态文件系统\n",[201,863,865,867,870,873,875,878,880,883,886,889,892,895,898],{"class":203,"line":864},14,[201,866,839],{"class":214},[201,868,869],{"class":228}," Dist ",[201,871,872],{"class":291},"http",[201,874,847],{"class":228},[201,876,877],{"class":291},"FileSystem",[201,879,225],{"class":214},[201,881,882],{"class":214}," &",[201,884,885],{"class":291},"fileSystem",[201,887,888],{"class":228},"{prefix: ",[201,890,891],{"class":238},"\"dist\"",[201,893,894],{"class":228},", fs: ",[201,896,897],{"class":214},"&",[201,899,900],{"class":228},"distFS}\n",[201,902,904],{"class":203,"line":903},15,[201,905,779],{"emptyLinePlaceholder":394},[201,907,909,912,915,918],{"class":203,"line":908},16,[201,910,911],{"class":214},"type",[201,913,914],{"class":291}," fileSystem",[201,916,917],{"class":214}," struct",[201,919,229],{"class":228},[201,921,923,926],{"class":203,"line":922},17,[201,924,925],{"class":228},"    prefix ",[201,927,928],{"class":214},"string\n",[201,930,932,935,938,940,942],{"class":203,"line":931},18,[201,933,934],{"class":228},"    fs     ",[201,936,937],{"class":214},"*",[201,939,795],{"class":291},[201,941,847],{"class":228},[201,943,850],{"class":291},[201,945,947],{"class":203,"line":946},19,[201,948,276],{"class":228},[201,950,952],{"class":203,"line":951},20,[201,953,779],{"emptyLinePlaceholder":394},[201,955,957,960,963,967,969,971,974,977,980,983,986,989,991,993,996,998,1001],{"class":203,"line":956},21,[201,958,959],{"class":214},"func",[201,961,962],{"class":228}," (",[201,964,966],{"class":965},"sPK6S","f ",[201,968,937],{"class":214},[201,970,885],{"class":291},[201,972,973],{"class":228},") ",[201,975,976],{"class":291},"Open",[201,978,979],{"class":228},"(",[201,981,982],{"class":965},"name",[201,984,985],{"class":214}," string",[201,987,988],{"class":228},") (",[201,990,872],{"class":291},[201,992,847],{"class":228},[201,994,995],{"class":291},"File",[201,997,338],{"class":228},[201,999,1000],{"class":214},"error",[201,1002,1003],{"class":228},") {\n",[201,1005,1007,1010,1013,1016,1019],{"class":203,"line":1006},22,[201,1008,1009],{"class":214},"    if",[201,1011,1012],{"class":228}," name ",[201,1014,1015],{"class":214},"==",[201,1017,1018],{"class":238}," \"\u002F\"",[201,1020,229],{"class":228},[201,1022,1024,1027,1030],{"class":203,"line":1023},23,[201,1025,1026],{"class":228},"        name ",[201,1028,1029],{"class":214},"=",[201,1031,1032],{"class":228}," f.prefix\n",[201,1034,1036,1039,1042],{"class":203,"line":1035},24,[201,1037,1038],{"class":228},"    } ",[201,1040,1041],{"class":214},"else",[201,1043,229],{"class":228},[201,1045,1047,1049,1051,1054,1057,1059,1062,1065,1068,1071,1074],{"class":203,"line":1046},25,[201,1048,1026],{"class":228},[201,1050,1029],{"class":214},[201,1052,1053],{"class":228}," f.prefix ",[201,1055,1056],{"class":214},"+",[201,1058,1018],{"class":238},[201,1060,1061],{"class":214}," +",[201,1063,1064],{"class":228}," strings.",[201,1066,1067],{"class":291},"TrimPrefix",[201,1069,1070],{"class":228},"(name, ",[201,1072,1073],{"class":238},"\"\u002F\"",[201,1075,822],{"class":228},[201,1077,1079],{"class":203,"line":1078},26,[201,1080,1081],{"class":228},"    }\n",[201,1083,1085,1088,1091,1094,1097,1099],{"class":203,"line":1084},27,[201,1086,1087],{"class":214},"    return",[201,1089,1090],{"class":228}," http.",[201,1092,1093],{"class":291},"FS",[201,1095,1096],{"class":228},"(f.fs).",[201,1098,976],{"class":291},[201,1100,1101],{"class":228},"(name)\n",[201,1103,1105],{"class":203,"line":1104},28,[201,1106,276],{"class":228},[17,1108,1109],{},"两个细节：",[121,1111,1112,1136],{},[124,1113,1114,1122,1123,1125,1126,1128,1129,1131,1132,1135],{},[21,1115,1116,1119,1120,533],{},[198,1117,1118],{},"all:dist"," 里的 ",[198,1121,532],{},"：embed 目录时默认会排除 ",[198,1124,537],{}," 和 ",[198,1127,847],{}," 开头的文件（上篇坑二就是被这个坑了，当时靠改 Vite 输出名绕开）。",[198,1130,532],{}," 前缀表示「全部包含」，前端构建产物里那些 ",[198,1133,1134],{},"_xxx.js"," 助手文件直接安全落地，不用再改前端配置；",[124,1137,1138,1141,1142,1144,1145,1148],{},[198,1139,1140],{},"http.FileSystem"," 包装：加了一层 ",[198,1143,558],{}," 前缀映射，",[198,1146,1147],{},"http.FileServer"," 和 Gin 都能直接用。",[564,1150,1152],{"id":1151},"_4-路由静态服务-缓存策略","4. 路由：静态服务 + 缓存策略",[191,1154,1156],{"className":757,"code":1155,"language":759,"meta":196,"style":196},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\nfunc NewRouter(db *gorm.DB, uploadDir string) *gin.Engine {\n    r := gin.Default()\n    ...\n    dist := http.FileServer(web.Dist)\n    r.GET(\"\u002Fdist\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    r.GET(\"\u002Fdist\u002F*filepath\", func(c *gin.Context) {\n        p := c.Param(\"filepath\")\n        if p == \"\u002F\" || p == \"\u002Findex.html\" {\n            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n            c.Header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n        } else if strings.HasPrefix(p, \"\u002Fassets\u002F\") {\n            \u002F\u002F 带内容 hash 的资源可放心长缓存\n            c.Header(\"Cache-Control\", \"public, max-age=31536000, immutable\")\n        }\n        http.StripPrefix(\"\u002Fdist\", dist).ServeHTTP(c.Writer, c.Request)\n    })\n    r.GET(\"\u002F\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    ...\n}\n",[198,1157,1158,1163,1207,1224,1229,1244,1293,1322,1342,1366,1371,1391,1414,1419,1436,1441,1462,1467,1507,1511],{"__ignoreMap":196},[201,1159,1160],{"class":203,"line":204},[201,1161,1162],{"class":207},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\n",[201,1164,1165,1167,1170,1172,1175,1178,1181,1183,1186,1188,1191,1193,1195,1197,1200,1202,1205],{"class":203,"line":211},[201,1166,959],{"class":214},[201,1168,1169],{"class":291}," NewRouter",[201,1171,979],{"class":228},[201,1173,1174],{"class":965},"db",[201,1176,1177],{"class":214}," *",[201,1179,1180],{"class":291},"gorm",[201,1182,847],{"class":228},[201,1184,1185],{"class":291},"DB",[201,1187,338],{"class":228},[201,1189,1190],{"class":965},"uploadDir",[201,1192,985],{"class":214},[201,1194,973],{"class":228},[201,1196,937],{"class":214},[201,1198,1199],{"class":291},"gin",[201,1201,847],{"class":228},[201,1203,1204],{"class":291},"Engine",[201,1206,229],{"class":228},[201,1208,1209,1212,1215,1218,1221],{"class":203,"line":232},[201,1210,1211],{"class":228},"    r ",[201,1213,1214],{"class":214},":=",[201,1216,1217],{"class":228}," gin.",[201,1219,1220],{"class":291},"Default",[201,1222,1223],{"class":228},"()\n",[201,1225,1226],{"class":203,"line":245},[201,1227,1228],{"class":214},"    ...\n",[201,1230,1231,1234,1236,1238,1241],{"class":203,"line":256},[201,1232,1233],{"class":228},"    dist ",[201,1235,1214],{"class":214},[201,1237,1090],{"class":228},[201,1239,1240],{"class":291},"FileServer",[201,1242,1243],{"class":228},"(web.Dist)\n",[201,1245,1246,1249,1252,1254,1257,1259,1261,1263,1266,1268,1270,1272,1275,1278,1281,1283,1286,1288,1290],{"class":203,"line":267},[201,1247,1248],{"class":228},"    r.",[201,1250,1251],{"class":291},"GET",[201,1253,979],{"class":228},[201,1255,1256],{"class":238},"\"\u002Fdist\"",[201,1258,338],{"class":228},[201,1260,959],{"class":214},[201,1262,979],{"class":228},[201,1264,1265],{"class":965},"c",[201,1267,1177],{"class":214},[201,1269,1199],{"class":291},[201,1271,847],{"class":228},[201,1273,1274],{"class":291},"Context",[201,1276,1277],{"class":228},") { c.",[201,1279,1280],{"class":291},"Redirect",[201,1282,979],{"class":228},[201,1284,1285],{"class":221},"302",[201,1287,338],{"class":228},[201,1289,611],{"class":238},[201,1291,1292],{"class":228},") })\n",[201,1294,1295,1297,1299,1301,1304,1306,1308,1310,1312,1314,1316,1318,1320],{"class":203,"line":273},[201,1296,1248],{"class":228},[201,1298,1251],{"class":291},[201,1300,979],{"class":228},[201,1302,1303],{"class":238},"\"\u002Fdist\u002F*filepath\"",[201,1305,338],{"class":228},[201,1307,959],{"class":214},[201,1309,979],{"class":228},[201,1311,1265],{"class":965},[201,1313,1177],{"class":214},[201,1315,1199],{"class":291},[201,1317,847],{"class":228},[201,1319,1274],{"class":291},[201,1321,1003],{"class":228},[201,1323,1324,1327,1329,1332,1335,1337,1340],{"class":203,"line":819},[201,1325,1326],{"class":228},"        p ",[201,1328,1214],{"class":214},[201,1330,1331],{"class":228}," c.",[201,1333,1334],{"class":291},"Param",[201,1336,979],{"class":228},[201,1338,1339],{"class":238},"\"filepath\"",[201,1341,822],{"class":228},[201,1343,1344,1347,1350,1352,1354,1357,1359,1361,1364],{"class":203,"line":825},[201,1345,1346],{"class":214},"        if",[201,1348,1349],{"class":228}," p ",[201,1351,1015],{"class":214},[201,1353,1018],{"class":238},[201,1355,1356],{"class":214}," ||",[201,1358,1349],{"class":228},[201,1360,1015],{"class":214},[201,1362,1363],{"class":238}," \"\u002Findex.html\"",[201,1365,229],{"class":228},[201,1367,1368],{"class":203,"line":830},[201,1369,1370],{"class":207},"            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n",[201,1372,1373,1376,1379,1381,1384,1386,1389],{"class":203,"line":836},[201,1374,1375],{"class":228},"            c.",[201,1377,1378],{"class":291},"Header",[201,1380,979],{"class":228},[201,1382,1383],{"class":238},"\"Cache-Control\"",[201,1385,338],{"class":228},[201,1387,1388],{"class":238},"\"no-cache, no-store, must-revalidate\"",[201,1390,822],{"class":228},[201,1392,1393,1396,1398,1401,1403,1406,1409,1412],{"class":203,"line":853},[201,1394,1395],{"class":228},"        } ",[201,1397,1041],{"class":214},[201,1399,1400],{"class":214}," if",[201,1402,1064],{"class":228},[201,1404,1405],{"class":291},"HasPrefix",[201,1407,1408],{"class":228},"(p, ",[201,1410,1411],{"class":238},"\"\u002Fassets\u002F\"",[201,1413,1003],{"class":228},[201,1415,1416],{"class":203,"line":858},[201,1417,1418],{"class":207},"            \u002F\u002F 带内容 hash 的资源可放心长缓存\n",[201,1420,1421,1423,1425,1427,1429,1431,1434],{"class":203,"line":864},[201,1422,1375],{"class":228},[201,1424,1378],{"class":291},[201,1426,979],{"class":228},[201,1428,1383],{"class":238},[201,1430,338],{"class":228},[201,1432,1433],{"class":238},"\"public, max-age=31536000, immutable\"",[201,1435,822],{"class":228},[201,1437,1438],{"class":203,"line":903},[201,1439,1440],{"class":228},"        }\n",[201,1442,1443,1446,1449,1451,1453,1456,1459],{"class":203,"line":908},[201,1444,1445],{"class":228},"        http.",[201,1447,1448],{"class":291},"StripPrefix",[201,1450,979],{"class":228},[201,1452,1256],{"class":238},[201,1454,1455],{"class":228},", dist).",[201,1457,1458],{"class":291},"ServeHTTP",[201,1460,1461],{"class":228},"(c.Writer, c.Request)\n",[201,1463,1464],{"class":203,"line":922},[201,1465,1466],{"class":228},"    })\n",[201,1468,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1489,1491,1493,1495,1497,1499,1501,1503,1505],{"class":203,"line":931},[201,1470,1248],{"class":228},[201,1472,1251],{"class":291},[201,1474,979],{"class":228},[201,1476,1073],{"class":238},[201,1478,338],{"class":228},[201,1480,959],{"class":214},[201,1482,979],{"class":228},[201,1484,1265],{"class":965},[201,1486,1177],{"class":214},[201,1488,1199],{"class":291},[201,1490,847],{"class":228},[201,1492,1274],{"class":291},[201,1494,1277],{"class":228},[201,1496,1280],{"class":291},[201,1498,979],{"class":228},[201,1500,1285],{"class":221},[201,1502,338],{"class":228},[201,1504,611],{"class":238},[201,1506,1292],{"class":228},[201,1508,1509],{"class":203,"line":946},[201,1510,1228],{"class":214},[201,1512,1513],{"class":203,"line":951},[201,1514,276],{"class":228},[17,1516,1517],{},"缓存策略是内嵌发布的必修课（坑二细说）：入口禁缓存、hash 资源长缓存。",[564,1519,1521],{"id":1520},"_5-打包脚本windows-版-linux-交叉编译版","5. 打包脚本：Windows 版 + Linux 交叉编译版",[191,1523,1525],{"className":357,"code":1524,"language":359,"meta":196,"style":196},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\nset -euo pipefail\ncd \"$(dirname \"$0\")\"\n\nBINARY=\"${BINARY:-magic-img}\"\n\necho \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n(cd frontend && npm run build)\n\necho \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n(cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver)\nchmod +x \"backend\u002F$BINARY\"\n\necho \"[3\u002F3] 打包完成: backend\u002F$BINARY\"\n",[198,1526,1527,1532,1537,1548,1568,1572,1593,1597,1605,1628,1632,1639,1699,1714,1718],{"__ignoreMap":196},[201,1528,1529],{"class":203,"line":204},[201,1530,1531],{"class":207},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[201,1533,1534],{"class":203,"line":211},[201,1535,1536],{"class":207},"# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\n",[201,1538,1539,1542,1545],{"class":203,"line":232},[201,1540,1541],{"class":221},"set",[201,1543,1544],{"class":221}," -euo",[201,1546,1547],{"class":238}," pipefail\n",[201,1549,1550,1553,1556,1559,1562,1565],{"class":203,"line":245},[201,1551,1552],{"class":221},"cd",[201,1554,1555],{"class":238}," \"$(",[201,1557,1558],{"class":291},"dirname",[201,1560,1561],{"class":238}," \"",[201,1563,1564],{"class":221},"$0",[201,1566,1567],{"class":238},"\")\"\n",[201,1569,1570],{"class":203,"line":256},[201,1571,779],{"emptyLinePlaceholder":394},[201,1573,1574,1577,1579,1582,1584,1587,1590],{"class":203,"line":267},[201,1575,1576],{"class":228},"BINARY",[201,1578,1029],{"class":214},[201,1580,1581],{"class":238},"\"${",[201,1583,1576],{"class":228},[201,1585,1586],{"class":214},":-",[201,1588,1589],{"class":228},"magic-img",[201,1591,1592],{"class":238},"}\"\n",[201,1594,1595],{"class":203,"line":273},[201,1596,779],{"emptyLinePlaceholder":394},[201,1598,1599,1602],{"class":203,"line":819},[201,1600,1601],{"class":221},"echo",[201,1603,1604],{"class":238}," \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n",[201,1606,1607,1609,1611,1614,1617,1620,1623,1626],{"class":203,"line":825},[201,1608,979],{"class":228},[201,1610,1552],{"class":221},[201,1612,1613],{"class":238}," frontend",[201,1615,1616],{"class":228}," && ",[201,1618,1619],{"class":291},"npm",[201,1621,1622],{"class":238}," run",[201,1624,1625],{"class":238}," build",[201,1627,822],{"class":228},[201,1629,1630],{"class":203,"line":830},[201,1631,779],{"emptyLinePlaceholder":394},[201,1633,1634,1636],{"class":203,"line":836},[201,1635,1601],{"class":221},[201,1637,1638],{"class":238}," \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n",[201,1640,1641,1643,1645,1648,1651,1653,1656,1659,1661,1664,1667,1669,1672,1675,1677,1680,1682,1685,1688,1691,1694,1697],{"class":203,"line":853},[201,1642,979],{"class":228},[201,1644,1552],{"class":221},[201,1646,1647],{"class":238}," backend",[201,1649,1650],{"class":228}," && CGO_ENABLED",[201,1652,1029],{"class":214},[201,1654,1655],{"class":238},"0",[201,1657,1658],{"class":228}," GOOS",[201,1660,1029],{"class":214},[201,1662,1663],{"class":238},"linux",[201,1665,1666],{"class":228}," GOARCH",[201,1668,1029],{"class":214},[201,1670,1671],{"class":238},"amd64",[201,1673,1674],{"class":291}," go",[201,1676,1625],{"class":238},[201,1678,1679],{"class":221}," -o",[201,1681,1561],{"class":238},[201,1683,1684],{"class":228},"$BINARY",[201,1686,1687],{"class":238},"\"",[201,1689,1690],{"class":221}," -ldflags=",[201,1692,1693],{"class":238},"\"-s -w\"",[201,1695,1696],{"class":238}," .\u002Fcmd\u002Fserver",[201,1698,822],{"class":228},[201,1700,1701,1704,1707,1710,1712],{"class":203,"line":858},[201,1702,1703],{"class":291},"chmod",[201,1705,1706],{"class":238}," +x",[201,1708,1709],{"class":238}," \"backend\u002F",[201,1711,1684],{"class":228},[201,1713,798],{"class":238},[201,1715,1716],{"class":203,"line":864},[201,1717,779],{"emptyLinePlaceholder":394},[201,1719,1720,1722,1725,1727],{"class":203,"line":903},[201,1721,1601],{"class":221},[201,1723,1724],{"class":238}," \"[3\u002F3] 打包完成: backend\u002F",[201,1726,1684],{"class":228},[201,1728,798],{"class":238},[17,1730,1731,1732,1735,1736,1739],{},"Windows 版（",[198,1733,1734],{},"build-win.bat","）同理，只是产物叫 ",[198,1737,1738],{},"magic-img.exe","。",[12,1741,1742],{"id":1742},"踩坑记录",[564,1744,1746,1747,1750,1751,1753],{"id":1745},"坑一goembed-dist-静默漏掉-_-开头文件升级解法","坑一：",[198,1748,1749],{},"\u002F\u002Fgo:embed dist"," 静默漏掉 ",[198,1752,537],{}," 开头文件（升级解法）",[17,1755,1756,1757,1765,1766,1768],{},"上篇说过：embed 目录时",[21,1758,1759,1760,1125,1762,1764],{},"硬编码排除 ",[198,1761,537],{},[198,1763,847],{}," 开头文件","。上篇的解法是改 Vite 输出文件名去掉 ",[198,1767,537],{}," 前缀；这次直接用：",[191,1770,1772],{"className":757,"code":1771,"language":759,"meta":196,"style":196},"\u002F\u002Fgo:embed all:dist   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[198,1773,1774],{"__ignoreMap":196},[201,1775,1776,1779],{"class":203,"line":204},[201,1777,1778],{"class":207},"\u002F\u002Fgo:embed all:dist",[201,1780,1781],{"class":207},"   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[17,1783,1784],{},[21,1785,1786,1787,1789,1790,1792],{},"结论：能用 ",[198,1788,532],{}," 就用 ",[198,1791,532],{},"，从根上消灭这类\"文件莫名 404\"的问题。",[564,1794,1796],{"id":1795},"坑二开发版是新的打包版却是旧的","坑二：开发版是新的、打包版却是旧的",[17,1798,1799,1800,1803],{},"现象：Vite 开发服务一切正常，",[198,1801,1802],{},":8080\u002Fdist\u002F"," 打包版却是旧界面，强刷也不一定好。",[17,1805,1806,1807,1810,1811,1814,1815,1820],{},"排查过程值得记录：先是怀疑 ",[198,1808,1809],{},"go build"," 缓存了旧 embed（上篇坑三的思路），重编重启无效；然后直接抓取 8080 返回的 HTML 和 JS，发现",[21,1812,1813],{},"服务端内容已经是新的","——问题出在",[21,1816,1817,1818],{},"浏览器缓存了 ",[198,1819,682],{},"：它引用旧 hash 的资源，一直不更新。",[17,1822,1823,1826,1827,1830,1831,1834],{},[21,1824,1825],{},"解决","：入口文件响应 ",[198,1828,1829],{},"Cache-Control: no-store","（见上面路由代码），带 hash 的资源给 ",[198,1832,1833],{},"immutable"," 长缓存。升级后浏览器每次拉新入口，资源文件名一变就自动换新，两全其美。",[1836,1837,1838],"blockquote",{},[17,1839,1840],{},"教训：排查\"新旧不一致\"先分清是哪一层——服务端产物、浏览器入口缓存、还是资源缓存。抓包看响应内容再动手。",[564,1842,1844],{"id":1843},"坑三windows-上跑的-build-linuxsh产出的文件-linux-却执行不了","坑三：Windows 上跑的 build-linux.sh，产出的文件 Linux 却执行不了",[17,1846,1847,1848,1851,1852,1858,1859,1862],{},"最初脚本只写了 ",[198,1849,1850],{},"CGO_ENABLED=0 go build","，",[21,1853,1854,1855],{},"没指定 ",[198,1856,1857],{},"GOOS\u002FGOARCH","。在 Windows 上跑，产出的其实是个 Windows PE 文件（只是没叫 ",[198,1860,1861],{},".exe","），传到 Linux 自然「无法执行」。",[17,1864,1865,1867],{},[21,1866,1825],{},"：脚本里显式交叉编译：",[191,1869,1871],{"className":357,"code":1870,"language":359,"meta":196,"style":196},"CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver\n",[198,1872,1873],{"__ignoreMap":196},[201,1874,1875,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896,1898,1900,1902,1904,1906,1908,1910],{"class":203,"line":204},[201,1876,1877],{"class":228},"CGO_ENABLED",[201,1879,1029],{"class":214},[201,1881,1655],{"class":238},[201,1883,1658],{"class":228},[201,1885,1029],{"class":214},[201,1887,1663],{"class":238},[201,1889,1666],{"class":228},[201,1891,1029],{"class":214},[201,1893,1671],{"class":238},[201,1895,1674],{"class":291},[201,1897,1625],{"class":238},[201,1899,1679],{"class":221},[201,1901,1561],{"class":238},[201,1903,1684],{"class":228},[201,1905,1687],{"class":238},[201,1907,1690],{"class":221},[201,1909,1693],{"class":238},[201,1911,1912],{"class":238}," .\u002Fcmd\u002Fserver\n",[17,1914,1915,1916,1919,1920,1923,1924],{},"能这么干的前提是",[21,1917,1918],{},"全依赖树无 cgo","——SQLite 用的是 modernc 纯 Go 驱动，bcrypt 用 ",[198,1921,1922],{},"x\u002Fcrypto","。这也是选型时就要留意的点：",[21,1925,1926],{},"想跨平台交叉编译，先把 cgo 依赖清干净。",[17,1928,1929,1930,1933,1934,1937],{},"另外两个小提醒：Windows 上编出的文件没有 Unix 执行位，上传服务器后记得 ",[198,1931,1932],{},"chmod +x","；",[198,1935,1936],{},"-ldflags=\"-s -w\""," 剥离符号，体积从 30MB+ 降到 26MB。",[564,1939,1941],{"id":1940},"坑四dist-被-gitignore-后embed-直接编译失败","坑四：dist 被 gitignore 后，embed 直接编译失败",[17,1943,1944,1946,1947,1949,1950,1953,1954,1956],{},[198,1945,707],{}," 是构建产物、入了 .gitignore，但 ",[198,1948,554],{}," 在",[21,1951,1952],{},"编译期","就要读它——刚 clone 下来直接 ",[198,1955,1809],{}," 会报 embed 找不到文件。",[17,1958,1959,1961,1962,1965],{},[21,1960,1825],{},"：gitignore 里保留 ",[198,1963,1964],{},"dist\u002F.gitkeep","（一个空文件），让目录存在于版本库中：",[191,1967,1971],{"className":1968,"code":1969,"language":1970,"meta":196,"style":196},"language-gitignore shiki shiki-themes github-light github-dark","backend\u002Fweb\u002Fdist\u002F*\n!backend\u002Fweb\u002Fdist\u002F\n!backend\u002Fweb\u002Fdist\u002F.gitkeep\n","gitignore",[198,1972,1973,1978,1983],{"__ignoreMap":196},[201,1974,1975],{"class":203,"line":204},[201,1976,1977],{},"backend\u002Fweb\u002Fdist\u002F*\n",[201,1979,1980],{"class":203,"line":211},[201,1981,1982],{},"!backend\u002Fweb\u002Fdist\u002F\n",[201,1984,1985],{"class":203,"line":232},[201,1986,1987],{},"!backend\u002Fweb\u002Fdist\u002F.gitkeep\n",[17,1989,1990,1991,1993,1994,1996],{},"这样 clone 后可以先 ",[198,1992,1809],{}," 通过，再 ",[198,1995,700],{}," 灌入真实前端。",[564,1998,2000],{"id":1999},"坑五500kb-chunk-警告别慌","坑五：>500KB chunk 警告，别慌",[17,2002,2003,2004,2007,2008,1739],{},"Vite 构建会警告单 chunk 超过 500KB。这个项目里 Base UI 组件库全量引入，单 chunk 约 640KB——",[21,2005,2006],{},"属正常，不要为了消警告去拆配置","。真要优化就上路由级 ",[198,2009,2010],{},"React.lazy",[12,2012,2013],{"id":2013},"最终效果",[191,2015,2017],{"className":357,"code":2016,"language":359,"meta":196,"style":196},"$ build-win.bat        # backendmagic-img.exe\n$ bash build-linux.sh  # backendmagic-img（linux\u002Famd64 ELF）\n",[198,2018,2019,2030],{"__ignoreMap":196},[201,2020,2021,2024,2027],{"class":203,"line":204},[201,2022,2023],{"class":291},"$",[201,2025,2026],{"class":238}," build-win.bat",[201,2028,2029],{"class":207},"        # backendmagic-img.exe\n",[201,2031,2032,2034,2037,2040],{"class":203,"line":211},[201,2033,2023],{"class":291},[201,2035,2036],{"class":238}," bash",[201,2038,2039],{"class":238}," build-linux.sh",[201,2041,2042],{"class":207},"  # backendmagic-img（linux\u002Famd64 ELF）\n",[17,2044,2045,2046,2049,2050,1739],{},"一个约 26MB 的二进制，内嵌 React 前端、Gin API、SQLite 数据初始化，拷到 Windows\u002FLinux 机器直接跑，浏览器访问 ",[198,2047,2048],{},"http:\u002F\u002Flocalhost:8080"," 自动重定向到 ",[198,2051,686],{},[12,2053,2054],{"id":2054},"总结",[17,2056,2057],{},"这轮相比上篇的三点升级：",[509,2059,2060,2066,2076],{},[124,2061,2062,2065],{},[21,2063,2064],{},"hash 路由 + base \u002Fdist","：省掉 SPA fallback，静态服务即开即用；",[124,2067,2068,578,2073,2075],{},[21,2069,2070,2072],{},[198,2071,532],{}," embed 前缀",[198,2074,537],{}," 开头文件问题一劳永逸；",[124,2077,2078,2084],{},[21,2079,2080,2081,2083],{},"显式 ",[198,2082,1857],{}," 交叉编译","：让「一个脚本、任意平台、产出 Linux 二进制」成为现实。",[17,2086,2087,2088,2091],{},"加上",[21,2089,2090],{},"入口 no-store \u002F 资源 immutable"," 的缓存策略，单文件分发的体验才算闭环——不然每次升级都要和浏览器缓存打架。",[438,2093,2094],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":196,"searchDepth":211,"depth":232,"links":2096},[2097,2098,2099,2106,2114,2115],{"id":473,"depth":211,"text":473},{"id":548,"depth":211,"text":548},{"id":562,"depth":211,"text":562,"children":2100},[2101,2102,2103,2104,2105],{"id":566,"depth":232,"text":567},{"id":694,"depth":232,"text":695},{"id":753,"depth":232,"text":754},{"id":1151,"depth":232,"text":1152},{"id":1520,"depth":232,"text":1521},{"id":1742,"depth":211,"text":1742,"children":2107},[2108,2110,2111,2112,2113],{"id":1745,"depth":232,"text":2109},"坑一：\u002F\u002Fgo:embed dist 静默漏掉 _ 开头文件（升级解法）",{"id":1795,"depth":232,"text":1796},{"id":1843,"depth":232,"text":1844},{"id":1940,"depth":232,"text":1941},{"id":1999,"depth":232,"text":2000},{"id":2013,"depth":211,"text":2013},{"id":2054,"depth":211,"text":2054},"以 Magic Img（React 19 + Vite + Gin + SQLite）为实例，记录把前端塞进 Go 二进制、并在 Windows 上交叉编译 Linux 产物的完整过程：embed 的 all 前缀、hash 路由、缓存策略、GOOS 交叉编译等实操坑。","进阶","\u002Fposts\u002Fgo-embed-react-single-binary\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-embed-react-single-binary","2026-08-16T00:00:00",{"title":468,"description":2116},"Go 工程实践","posts\u002Fgo-embed-react-single-binary\u002Findex",[2126,2127,795,2128,2129],"Go","React","交叉编译","打包","2026-08-16","WV-T-MOO5Cu-aycw-Ony8s9hjA3_k0S4XU-T0QsCow0",{"id":2133,"title":2134,"author":7,"body":2135,"description":3406,"difficulty":451,"draft":452,"extension":453,"image":3407,"meta":3408,"navigation":394,"path":3409,"pinned":452,"published":2121,"seo":3410,"series":3411,"seriesOrder":204,"stem":3412,"tags":3413,"updated":2130,"__hash__":3417},"posts\u002Fposts\u002Freact-photo-album-masonry\u002Findex.md","用 react-photo-album 给生图图库做瀑布流排版",{"type":9,"value":2136,"toc":3389},[2137,2140,2143,2161,2168,2235,2246,2249,2272,2275,2279,2294,2298,2316,2501,2505,2908,2911,2945,2948,2952,2962,2968,2993,2998,3009,3015,3026,3029,3069,3082,3206,3218,3222,3229,3247,3252,3347,3353,3357,3368,3371,3374,3380,3386],[12,2138,2139],{"id":2139},"技术背景",[17,2141,2142],{},"Magic Img 是我做的一个 AI 生图提示词管理工具（自托管、本地优先），其中「生图图库」页面要把所有生成图片以图片墙的形式展示出来。图片墙有两个天然矛盾：",[509,2144,2145,2151],{},[124,2146,2147,2150],{},[21,2148,2149],{},"等宽网格","（CSS Grid）：整齐，但每行高度由最高的那张决定——不同比例的图片混排时，矮图下面会留下大洞，看起来\"很乱\"；",[124,2152,2153,2156,2157,2160],{},[21,2154,2155],{},"原生比例展示","：图库必须不裁剪（这是产品约束），所以不能用 ",[198,2158,2159],{},"object-cover"," 强行统一比例。",[17,2162,2163,2164,2167],{},"业内的标准解法是 ",[21,2165,2166],{},"Masonry 瀑布流","：按列紧密堆放，每张图保持原始比例，列间高度错落但整体紧凑。实现瀑布流的常见方案有三类：",[35,2169,2170,2186],{},[38,2171,2172],{},[41,2173,2174,2177,2180,2183],{},[44,2175,2176],{},"方案",[44,2178,2179],{},"依赖",[44,2181,2182],{},"顺序",[44,2184,2185],{},"说明",[51,2187,2188,2206,2219],{},[41,2189,2190,2197,2200,2203],{},[56,2191,2192,2193,2196],{},"CSS ",[198,2194,2195],{},"columns"," 多列",[56,2198,2199],{},"无",[56,2201,2202],{},"纵向阅读序（先下后右）",[56,2204,2205],{},"最新图片不再出现在左上角，时间语义丢失",[41,2207,2208,2211,2213,2216],{},[56,2209,2210],{},"grid row-span 自研",[56,2212,2199],{},[56,2214,2215],{},"保持",[56,2217,2218],{},"需固定列数 + 固定行高，逻辑要自己写",[41,2220,2221,2226,2229,2232],{},[56,2222,2223],{},[21,2224,2225],{},"react-photo-album",[56,2227,2228],{},"~4KB，无依赖",[56,2230,2231],{},"基本保持（逐列最短列填充）",[56,2233,2234],{},"专为相册墙设计，TS 友好，维护活跃",[17,2236,2237,2238,2241,2242,2245],{},"最终选了 ",[21,2239,2240],{},"react-photo-album v3","：体积小、类型齐全，而且它的 Masonry 模式恰好需要「每张图的 ",[198,2243,2244],{},"width\u002Fheight","」——我们的数据库里本来就存了，零额外成本。",[12,2247,2248],{"id":2248},"需求梳理",[509,2250,2251,2257,2263,2266,2269],{},[124,2252,2253,2254,1933],{},"展示全部生成图（21 张起，会持续增长），",[21,2255,2256],{},"原比例、不裁剪",[124,2258,2259,2260,1933],{},"不同尺寸（2304×1728、4608×3456 等）混排时",[21,2261,2262],{},"紧凑、不留洞",[124,2264,2265],{},"顺序基本保持「新图靠前」；",[124,2267,2268],{},"点击任意一张打开大图预览（YARL 灯箱），悬停显示所属提示词；",[124,2270,2271],{},"平板性能：墙上只用 300px 缩略图，原图只进灯箱按需加载。",[12,2273,2274],{"id":2274},"安装与使用",[564,2276,2278],{"id":2277},"_1-安装","1. 安装",[191,2280,2282],{"className":357,"code":2281,"language":359,"meta":196,"style":196},"npm install react-photo-album\n",[198,2283,2284],{"__ignoreMap":196},[201,2285,2286,2288,2291],{"class":203,"line":204},[201,2287,1619],{"class":291},[201,2289,2290],{"class":238}," install",[201,2292,2293],{"class":238}," react-photo-album\n",[564,2295,2297],{"id":2296},"_2-准备数据关键输入是宽高","2. 准备数据（关键输入是宽高）",[17,2299,2300,2301,2304,2305,2308,2309,2308,2312,2315],{},"库要求的 ",[198,2302,2303],{},"Photo"," 对象只有三个必需字段：",[198,2306,2307],{},"src","、",[198,2310,2311],{},"width",[198,2313,2314],{},"height","。我们的接口本来就返回每张图的原始宽高：",[191,2317,2319],{"className":630,"code":2318,"language":632,"meta":196,"style":196},"import PhotoAlbum, { type Photo } from \"react-photo-album\"\n\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\nimport \"react-photo-album\u002Fstyles.css\"\n\ninterface GalleryPhoto extends Photo {\n  prompt_text?: string\n}\n\nconst albumPhotos: GalleryPhoto[] = images.map((img) => ({\n  key: String(img.id),\n  src: assetUrl(img.thumb_path) || assetUrl(img.file_path),\n  width: img.width,\n  height: img.height,\n  alt: img.prompt_text || `生成图 ${img.id}`,\n  prompt_text: img.prompt_text,\n}))\n",[198,2320,2321,2339,2344,2351,2355,2371,2382,2386,2390,2428,2439,2459,2464,2469,2491,2496],{"__ignoreMap":196},[201,2322,2323,2325,2328,2330,2333,2336],{"class":203,"line":204},[201,2324,784],{"class":214},[201,2326,2327],{"class":228}," PhotoAlbum, { ",[201,2329,911],{"class":214},[201,2331,2332],{"class":228}," Photo } ",[201,2334,2335],{"class":214},"from",[201,2337,2338],{"class":238}," \"react-photo-album\"\n",[201,2340,2341],{"class":203,"line":211},[201,2342,2343],{"class":207},"\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\n",[201,2345,2346,2348],{"class":203,"line":232},[201,2347,784],{"class":214},[201,2349,2350],{"class":238}," \"react-photo-album\u002Fstyles.css\"\n",[201,2352,2353],{"class":203,"line":245},[201,2354,779],{"emptyLinePlaceholder":394},[201,2356,2357,2360,2363,2366,2369],{"class":203,"line":256},[201,2358,2359],{"class":214},"interface",[201,2361,2362],{"class":291}," GalleryPhoto",[201,2364,2365],{"class":214}," extends",[201,2367,2368],{"class":291}," Photo",[201,2370,229],{"class":228},[201,2372,2373,2376,2379],{"class":203,"line":267},[201,2374,2375],{"class":965},"  prompt_text",[201,2377,2378],{"class":214},"?:",[201,2380,2381],{"class":221}," string\n",[201,2383,2384],{"class":203,"line":273},[201,2385,276],{"class":228},[201,2387,2388],{"class":203,"line":819},[201,2389,779],{"emptyLinePlaceholder":394},[201,2391,2392,2395,2398,2401,2403,2406,2408,2411,2414,2417,2420,2422,2425],{"class":203,"line":825},[201,2393,2394],{"class":214},"const",[201,2396,2397],{"class":221}," albumPhotos",[201,2399,2400],{"class":214},":",[201,2402,2362],{"class":291},[201,2404,2405],{"class":228},"[] ",[201,2407,1029],{"class":214},[201,2409,2410],{"class":228}," images.",[201,2412,2413],{"class":291},"map",[201,2415,2416],{"class":228},"((",[201,2418,2419],{"class":965},"img",[201,2421,973],{"class":228},[201,2423,2424],{"class":214},"=>",[201,2426,2427],{"class":228}," ({\n",[201,2429,2430,2433,2436],{"class":203,"line":830},[201,2431,2432],{"class":228},"  key: ",[201,2434,2435],{"class":291},"String",[201,2437,2438],{"class":228},"(img.id),\n",[201,2440,2441,2444,2447,2450,2453,2456],{"class":203,"line":836},[201,2442,2443],{"class":228},"  src: ",[201,2445,2446],{"class":291},"assetUrl",[201,2448,2449],{"class":228},"(img.thumb_path) ",[201,2451,2452],{"class":214},"||",[201,2454,2455],{"class":291}," assetUrl",[201,2457,2458],{"class":228},"(img.file_path),\n",[201,2460,2461],{"class":203,"line":853},[201,2462,2463],{"class":228},"  width: img.width,\n",[201,2465,2466],{"class":203,"line":858},[201,2467,2468],{"class":228},"  height: img.height,\n",[201,2470,2471,2474,2476,2479,2481,2483,2486,2489],{"class":203,"line":864},[201,2472,2473],{"class":228},"  alt: img.prompt_text ",[201,2475,2452],{"class":214},[201,2477,2478],{"class":238}," `生成图 ${",[201,2480,2419],{"class":228},[201,2482,847],{"class":238},[201,2484,2485],{"class":228},"id",[201,2487,2488],{"class":238},"}`",[201,2490,242],{"class":228},[201,2492,2493],{"class":203,"line":903},[201,2494,2495],{"class":228},"  prompt_text: img.prompt_text,\n",[201,2497,2498],{"class":203,"line":908},[201,2499,2500],{"class":228},"}))\n",[564,2502,2504],{"id":2503},"_3-使用-masonry-布局","3. 使用 Masonry 布局",[191,2506,2508],{"className":630,"code":2507,"language":632,"meta":196,"style":196},"\u003CPhotoAlbum\n  layout=\"masonry\"\n  columns={(cw) => (cw \u003C 640 ? 2 : cw \u003C 960 ? 3 : cw \u003C 1280 ? 4 : 5)}\n  spacing={16}\n  padding={0}\n  photos={albumPhotos}\n  onClick={({ index }) => openPreview(index)}\n  render={{\n    photo: (props, context) => {\n      const { onClick } = props\n      const { photo, index, width, height } = context\n      return (\n        \u003Cbutton type=\"button\" onClick={onClick} className=\"group relative block w-full ...\">\n          \u003Cimg\n            src={photo.src}\n            alt={photo.alt || \"\"}\n            decoding=\"async\"\n            style={{ width, height }}\n          \u002F>\n          {\u002F* hover 提示词浮层 *\u002F}\n          \u003Cdiv className=\"... opacity-0 group-hover:opacity-100\">\n            {photo.prompt_text}\n          \u003C\u002Fdiv>\n        \u003C\u002Fbutton>\n      )\n    },\n  }}\n\u002F>\n",[198,2509,2510,2517,2527,2596,2611,2624,2634,2658,2668,2690,2709,2737,2744,2778,2786,2796,2813,2823,2833,2838,2848,2865,2870,2879,2888,2893,2898,2903],{"__ignoreMap":196},[201,2511,2512,2514],{"class":203,"line":204},[201,2513,644],{"class":228},[201,2515,2516],{"class":221},"PhotoAlbum\n",[201,2518,2519,2522,2524],{"class":203,"line":211},[201,2520,2521],{"class":291},"  layout",[201,2523,1029],{"class":214},[201,2525,2526],{"class":238},"\"masonry\"\n",[201,2528,2529,2532,2534,2537,2540,2542,2544,2547,2549,2552,2555,2558,2561,2564,2566,2569,2571,2574,2576,2578,2580,2583,2585,2588,2590,2593],{"class":203,"line":232},[201,2530,2531],{"class":291},"  columns",[201,2533,1029],{"class":214},[201,2535,2536],{"class":228},"{(",[201,2538,2539],{"class":965},"cw",[201,2541,973],{"class":228},[201,2543,2424],{"class":214},[201,2545,2546],{"class":228}," (cw ",[201,2548,644],{"class":214},[201,2550,2551],{"class":221}," 640",[201,2553,2554],{"class":214}," ?",[201,2556,2557],{"class":221}," 2",[201,2559,2560],{"class":214}," :",[201,2562,2563],{"class":228}," cw ",[201,2565,644],{"class":214},[201,2567,2568],{"class":221}," 960",[201,2570,2554],{"class":214},[201,2572,2573],{"class":221}," 3",[201,2575,2560],{"class":214},[201,2577,2563],{"class":228},[201,2579,644],{"class":214},[201,2581,2582],{"class":221}," 1280",[201,2584,2554],{"class":214},[201,2586,2587],{"class":221}," 4",[201,2589,2560],{"class":214},[201,2591,2592],{"class":221}," 5",[201,2594,2595],{"class":228},")}\n",[201,2597,2598,2601,2603,2606,2609],{"class":203,"line":245},[201,2599,2600],{"class":291},"  spacing",[201,2602,1029],{"class":214},[201,2604,2605],{"class":228},"{",[201,2607,2608],{"class":221},"16",[201,2610,276],{"class":228},[201,2612,2613,2616,2618,2620,2622],{"class":203,"line":256},[201,2614,2615],{"class":291},"  padding",[201,2617,1029],{"class":214},[201,2619,2605],{"class":228},[201,2621,1655],{"class":221},[201,2623,276],{"class":228},[201,2625,2626,2629,2631],{"class":203,"line":267},[201,2627,2628],{"class":291},"  photos",[201,2630,1029],{"class":214},[201,2632,2633],{"class":228},"{albumPhotos}\n",[201,2635,2636,2639,2641,2644,2647,2650,2652,2655],{"class":203,"line":273},[201,2637,2638],{"class":291},"  onClick",[201,2640,1029],{"class":214},[201,2642,2643],{"class":228},"{({ ",[201,2645,2646],{"class":965},"index",[201,2648,2649],{"class":228}," }) ",[201,2651,2424],{"class":214},[201,2653,2654],{"class":291}," openPreview",[201,2656,2657],{"class":228},"(index)}\n",[201,2659,2660,2663,2665],{"class":203,"line":819},[201,2661,2662],{"class":291},"  render",[201,2664,1029],{"class":214},[201,2666,2667],{"class":228},"{{\n",[201,2669,2670,2673,2676,2679,2681,2684,2686,2688],{"class":203,"line":825},[201,2671,2672],{"class":291},"    photo",[201,2674,2675],{"class":228},": (",[201,2677,2678],{"class":965},"props",[201,2680,338],{"class":228},[201,2682,2683],{"class":965},"context",[201,2685,973],{"class":228},[201,2687,2424],{"class":214},[201,2689,229],{"class":228},[201,2691,2692,2695,2698,2701,2704,2706],{"class":203,"line":830},[201,2693,2694],{"class":214},"      const",[201,2696,2697],{"class":228}," { ",[201,2699,2700],{"class":221},"onClick",[201,2702,2703],{"class":228}," } ",[201,2705,1029],{"class":214},[201,2707,2708],{"class":228}," props\n",[201,2710,2711,2713,2715,2718,2720,2722,2724,2726,2728,2730,2732,2734],{"class":203,"line":836},[201,2712,2694],{"class":214},[201,2714,2697],{"class":228},[201,2716,2717],{"class":221},"photo",[201,2719,338],{"class":228},[201,2721,2646],{"class":221},[201,2723,338],{"class":228},[201,2725,2311],{"class":221},[201,2727,338],{"class":228},[201,2729,2314],{"class":221},[201,2731,2703],{"class":228},[201,2733,1029],{"class":214},[201,2735,2736],{"class":228}," context\n",[201,2738,2739,2742],{"class":203,"line":853},[201,2740,2741],{"class":214},"      return",[201,2743,787],{"class":228},[201,2745,2746,2749,2752,2755,2757,2760,2763,2765,2768,2771,2773,2776],{"class":203,"line":858},[201,2747,2748],{"class":228},"        \u003C",[201,2750,2751],{"class":297},"button",[201,2753,2754],{"class":291}," type",[201,2756,1029],{"class":214},[201,2758,2759],{"class":238},"\"button\"",[201,2761,2762],{"class":291}," onClick",[201,2764,1029],{"class":214},[201,2766,2767],{"class":228},"{onClick} ",[201,2769,2770],{"class":291},"className",[201,2772,1029],{"class":214},[201,2774,2775],{"class":238},"\"group relative block w-full ...\"",[201,2777,649],{"class":228},[201,2779,2780,2783],{"class":203,"line":864},[201,2781,2782],{"class":228},"          \u003C",[201,2784,2785],{"class":297},"img\n",[201,2787,2788,2791,2793],{"class":203,"line":903},[201,2789,2790],{"class":291},"            src",[201,2792,1029],{"class":214},[201,2794,2795],{"class":228},"{photo.src}\n",[201,2797,2798,2801,2803,2806,2808,2811],{"class":203,"line":908},[201,2799,2800],{"class":291},"            alt",[201,2802,1029],{"class":214},[201,2804,2805],{"class":228},"{photo.alt ",[201,2807,2452],{"class":214},[201,2809,2810],{"class":238}," \"\"",[201,2812,276],{"class":228},[201,2814,2815,2818,2820],{"class":203,"line":922},[201,2816,2817],{"class":291},"            decoding",[201,2819,1029],{"class":214},[201,2821,2822],{"class":238},"\"async\"\n",[201,2824,2825,2828,2830],{"class":203,"line":931},[201,2826,2827],{"class":291},"            style",[201,2829,1029],{"class":214},[201,2831,2832],{"class":228},"{{ width, height }}\n",[201,2834,2835],{"class":203,"line":946},[201,2836,2837],{"class":228},"          \u002F>\n",[201,2839,2840,2843,2846],{"class":203,"line":951},[201,2841,2842],{"class":228},"          {",[201,2844,2845],{"class":207},"\u002F* hover 提示词浮层 *\u002F",[201,2847,276],{"class":228},[201,2849,2850,2852,2855,2858,2860,2863],{"class":203,"line":956},[201,2851,2782],{"class":228},[201,2853,2854],{"class":297},"div",[201,2856,2857],{"class":291}," className",[201,2859,1029],{"class":214},[201,2861,2862],{"class":238},"\"... opacity-0 group-hover:opacity-100\"",[201,2864,649],{"class":228},[201,2866,2867],{"class":203,"line":1006},[201,2868,2869],{"class":228},"            {photo.prompt_text}\n",[201,2871,2872,2875,2877],{"class":203,"line":1023},[201,2873,2874],{"class":228},"          \u003C\u002F",[201,2876,2854],{"class":297},[201,2878,649],{"class":228},[201,2880,2881,2884,2886],{"class":203,"line":1035},[201,2882,2883],{"class":228},"        \u003C\u002F",[201,2885,2751],{"class":297},[201,2887,649],{"class":228},[201,2889,2890],{"class":203,"line":1046},[201,2891,2892],{"class":228},"      )\n",[201,2894,2895],{"class":203,"line":1078},[201,2896,2897],{"class":228},"    },\n",[201,2899,2900],{"class":203,"line":1084},[201,2901,2902],{"class":228},"  }}\n",[201,2904,2905],{"class":203,"line":1104},[201,2906,2907],{"class":228},"\u002F>\n",[17,2909,2910],{},"几个要点：",[121,2912,2913,2922,2935],{},[124,2914,2915,2917,2918,2921],{},[198,2916,2195],{}," 可以是数字或 ",[198,2919,2920],{},"(containerWidth) => number"," 函数，做响应式列数很方便；",[124,2923,2924,2925,2927,2928,2931,2932,2934],{},"点击回调 ",[198,2926,2700],{}," 里拿到的是",[21,2929,2930],{},"原始数组下标"," ",[198,2933,2646],{},"，直接传给灯箱即可；",[124,2936,2937,2938,2941,2942,2944],{},"自定义 ",[198,2939,2940],{},"render.photo"," 后，图片尺寸要自己用 context 里的 ",[198,2943,2244],{},"（渲染尺寸）设置——这两个值库已经按列宽算好了。",[12,2946,2947],{"id":2947},"遇到的坑",[564,2949,2951],{"id":2950},"坑-1忘记引入样式表-一张一列","坑 1：忘记引入样式表 → 「一张一列」",[17,2953,2954,2955,2958,2959,1739],{},"这是最坑的一个。装完包、写好 ",[198,2956,2957],{},"\u003CPhotoAlbum layout=\"masonry\">","，页面渲染出来却是：",[21,2960,2961],{},"每张图占一整行，右侧大片置灰空白",[17,2963,2964,2965,2967],{},"排查过程：先怀疑是 ",[198,2966,2195],{}," 函数没生效，改成固定数字无效；再检查 DOM——容器和 5 个列（track）都在，列的宽度也算对了，但图片全都堆在布局里。",[17,2969,2970,2971,2974,2975,2308,2978,2981,2982,2988,2989,2992],{},"最后翻包的 ",[198,2972,2973],{},"package.json"," 发现它导出了独立的 CSS 文件（",[198,2976,2977],{},".\u002Fstyles.css",[198,2979,2980],{},".\u002Fmasonry.css"," 等），而",[21,2983,2984,2985],{},"官方 README 示例都带了一行 ",[198,2986,2987],{},"import \"react-photo-album\u002Fstyles.css\"","。容器是 flex、列是 flex-column、列宽按 ",[198,2990,2991],{},"--react-photo-album--columns"," 计算——这些规则全在这个样式表里，不引入就退化成普通块级堆叠。",[17,2994,2995,2997],{},[21,2996,1825],{},"：顶部加一行：",[191,2999,3001],{"className":630,"code":3000,"language":632,"meta":196,"style":196},"import \"react-photo-album\u002Fstyles.css\"\n",[198,3002,3003],{"__ignoreMap":196},[201,3004,3005,3007],{"class":203,"line":204},[201,3006,784],{"class":214},[201,3008,2350],{"class":238},[17,3010,3011,3012],{},"经验：",[21,3013,3014],{},"用这类\"无运行时依赖\"的布局库，先看它的 exports 里有没有独立 CSS，有就一定要引。",[564,3016,3018,3019,3021,3022,3025],{"id":3017},"坑-2renderphoto-是-props-context-双参签名","坑 2：",[198,3020,2940],{}," 是 ",[198,3023,3024],{},"(props, context)"," 双参签名",[17,3027,3028],{},"自定义渲染时我凭直觉写了：",[191,3030,3032],{"className":630,"code":3031,"language":632,"meta":196,"style":196},"photo: ({ photo, index, width, height, onClick }) => ... \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[198,3033,3034],{"__ignoreMap":196},[201,3035,3036,3038,3041,3043,3045,3047,3049,3051,3053,3055,3057,3059,3061,3063,3066],{"class":203,"line":204},[201,3037,2717],{"class":291},[201,3039,3040],{"class":228},": ({ ",[201,3042,2717],{"class":965},[201,3044,338],{"class":228},[201,3046,2646],{"class":965},[201,3048,338],{"class":228},[201,3050,2311],{"class":965},[201,3052,338],{"class":228},[201,3054,2314],{"class":965},[201,3056,338],{"class":228},[201,3058,2700],{"class":965},[201,3060,2649],{"class":228},[201,3062,2424],{"class":214},[201,3064,3065],{"class":214}," ...",[201,3067,3068],{"class":207}," \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[17,3070,3071,3072,3074,3075,3078,3079,578],{},"TS 报错提示 ",[198,3073,2717],{}," 等属性不存在于 ",[198,3076,3077],{},"RenderPhotoProps","。看类型定义才发现 v3 的渲染函数是",[21,3080,3081],{},"两个参数",[191,3083,3085],{"className":630,"code":3084,"language":632,"meta":196,"style":196},"type RenderFunction\u003CProps, Context> = (props: Props, context: Context) => ReactNode\n\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\nphoto: (props, context) => {\n  const { onClick } = props\n  const { photo, index, width, height } = context\n  ...\n}\n",[198,3086,3087,3133,3138,3158,3173,3198,3202],{"__ignoreMap":196},[201,3088,3089,3091,3094,3096,3099,3101,3103,3106,3108,3110,3112,3114,3117,3119,3121,3123,3126,3128,3130],{"class":203,"line":204},[201,3090,911],{"class":214},[201,3092,3093],{"class":291}," RenderFunction",[201,3095,644],{"class":228},[201,3097,3098],{"class":291},"Props",[201,3100,338],{"class":228},[201,3102,1274],{"class":291},[201,3104,3105],{"class":228},"> ",[201,3107,1029],{"class":214},[201,3109,962],{"class":228},[201,3111,2678],{"class":965},[201,3113,2400],{"class":214},[201,3115,3116],{"class":291}," Props",[201,3118,338],{"class":228},[201,3120,2683],{"class":965},[201,3122,2400],{"class":214},[201,3124,3125],{"class":291}," Context",[201,3127,973],{"class":228},[201,3129,2424],{"class":214},[201,3131,3132],{"class":291}," ReactNode\n",[201,3134,3135],{"class":203,"line":211},[201,3136,3137],{"class":207},"\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\n",[201,3139,3140,3142,3144,3146,3148,3150,3152,3154,3156],{"class":203,"line":232},[201,3141,2717],{"class":291},[201,3143,2400],{"class":214},[201,3145,962],{"class":228},[201,3147,2678],{"class":965},[201,3149,338],{"class":228},[201,3151,2683],{"class":965},[201,3153,973],{"class":228},[201,3155,2424],{"class":214},[201,3157,229],{"class":228},[201,3159,3160,3163,3165,3167,3170],{"class":203,"line":245},[201,3161,3162],{"class":291},"  const",[201,3164,2697],{"class":228},[201,3166,2700],{"class":965},[201,3168,3169],{"class":228}," } = ",[201,3171,3172],{"class":965},"props\n",[201,3174,3175,3177,3179,3181,3183,3185,3187,3189,3191,3193,3195],{"class":203,"line":256},[201,3176,3162],{"class":291},[201,3178,2697],{"class":228},[201,3180,2717],{"class":965},[201,3182,338],{"class":228},[201,3184,2646],{"class":965},[201,3186,338],{"class":228},[201,3188,2311],{"class":965},[201,3190,338],{"class":228},[201,3192,2314],{"class":965},[201,3194,3169],{"class":228},[201,3196,3197],{"class":965},"context\n",[201,3199,3200],{"class":203,"line":267},[201,3201,622],{"class":214},[201,3203,3204],{"class":203,"line":273},[201,3205,276],{"class":228},[17,3207,3208,3210,3211,3213,3214,3217],{},[21,3209,1825],{},"：按 ",[198,3212,3024],{}," 拆解即可。装新库先扫一眼 ",[198,3215,3216],{},".d.ts"," 里的 Render 类型签名，能少走很多弯路。",[564,3219,3221],{"id":3220},"坑-3测试断言把dom-顺序当成了视觉顺序","坑 3：测试断言把「DOM 顺序」当成了「视觉顺序」",[17,3223,3224,3225,3228],{},"布局修好后，我在浏览器自测脚本里加了一条断言：取前两个 ",[198,3226,3227],{},"\u003Cimg>","，比较它们的左坐标，认为应该并排（不同列）。",[17,3230,3231,3232,3235,3236,1125,3239,3242,3243,3246],{},"结果断言失败——但页面明明是 5 列并排。排查发现：",[21,3233,3234],{},"瀑布流的 DOM 是按\"列\"组织的","（第 1 列的全部图片在前，第 2 列的在后），所以 ",[198,3237,3238],{},"img[0]",[198,3240,3241],{},"img[1]"," 属于",[21,3244,3245],{},"同一列","，左坐标当然相同。",[17,3248,3249,3251],{},[21,3250,1825],{},"：断言改为比较「第 1 列的首张图 vs 第 2 列的首张图」：",[191,3253,3257],{"className":3254,"code":3255,"language":3256,"meta":196,"style":196},"language-js shiki shiki-themes github-light github-dark","const a = tracks[0].querySelector(\"img\")\nconst b = tracks[1].querySelector(\"img\")\nreturn Math.abs(a.getBoundingClientRect().left - b.getBoundingClientRect().left) > 10\n","js",[198,3258,3259,3286,3310],{"__ignoreMap":196},[201,3260,3261,3263,3266,3268,3271,3273,3276,3279,3281,3284],{"class":203,"line":204},[201,3262,2394],{"class":214},[201,3264,3265],{"class":221}," a",[201,3267,225],{"class":214},[201,3269,3270],{"class":228}," tracks[",[201,3272,1655],{"class":221},[201,3274,3275],{"class":228},"].",[201,3277,3278],{"class":291},"querySelector",[201,3280,979],{"class":228},[201,3282,3283],{"class":238},"\"img\"",[201,3285,822],{"class":228},[201,3287,3288,3290,3293,3295,3297,3300,3302,3304,3306,3308],{"class":203,"line":211},[201,3289,2394],{"class":214},[201,3291,3292],{"class":221}," b",[201,3294,225],{"class":214},[201,3296,3270],{"class":228},[201,3298,3299],{"class":221},"1",[201,3301,3275],{"class":228},[201,3303,3278],{"class":291},[201,3305,979],{"class":228},[201,3307,3283],{"class":238},[201,3309,822],{"class":228},[201,3311,3312,3315,3318,3321,3324,3327,3330,3333,3336,3338,3341,3344],{"class":203,"line":232},[201,3313,3314],{"class":214},"return",[201,3316,3317],{"class":228}," Math.",[201,3319,3320],{"class":291},"abs",[201,3322,3323],{"class":228},"(a.",[201,3325,3326],{"class":291},"getBoundingClientRect",[201,3328,3329],{"class":228},"().left ",[201,3331,3332],{"class":214},"-",[201,3334,3335],{"class":228}," b.",[201,3337,3326],{"class":291},[201,3339,3340],{"class":228},"().left) ",[201,3342,3343],{"class":214},">",[201,3345,3346],{"class":221}," 10\n",[17,3348,3011,3349,3352],{},[21,3350,3351],{},"验证布局类库时，先搞清楚它生成的 DOM 结构，再写断言","；另外把「真实浏览器探针」当排查手段（直接打印各 track 的 x 坐标和子元素数量），比盯着代码猜快得多。",[564,3354,3356],{"id":3355},"坑-4等宽网格的洞引入前就存在的产品问题","坑 4：等宽网格的\"洞\"（引入前就存在的产品问题）",[17,3358,3359,3360,3363,3364,3367],{},"顺带记录一下为什么换掉原来的布局：CSS Grid ",[198,3361,3362],{},"repeat(auto-fill, minmax(220px, 1fr))"," 等宽铺满，图片 ",[198,3365,3366],{},"w-full h-auto","。当一行里混有不同比例时，行高取最高的那张，矮图底部留空，几行错落下来就显得乱；另外窗口宽度\"差一点放不下下一张\"时，右侧会空出近一整列。",[17,3369,3370],{},"换成 Masonry 后这两个问题都消失了——列内紧密堆叠、列数自适应。",[12,3372,3373],{"id":3373},"效果与小结",[17,3375,3376,3377,3379],{},"最终效果：21 张不同比例的生成图在 5 列瀑布流里紧密排列，原比例不裁剪，悬停出提示词浮层，点击进 YARL 大图预览；缩略图总大小仅 360KB，进页面即全量加载 + ",[198,3378,1833],{}," 长缓存，平板上快速滚动也跟手。",[17,3381,3382,3383],{},"一句话总结：",[21,3384,3385],{},"react-photo-album 是个小但专业的相册布局库，只要记住三件事——引样式表、喂宽高、render 函数是双参——就能在十分钟内把图片墙升级成真正的瀑布流。",[438,3387,3388],{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":196,"searchDepth":211,"depth":232,"links":3390},[3391,3392,3393,3398,3405],{"id":2139,"depth":211,"text":2139},{"id":2248,"depth":211,"text":2248},{"id":2274,"depth":211,"text":2274,"children":3394},[3395,3396,3397],{"id":2277,"depth":232,"text":2278},{"id":2296,"depth":232,"text":2297},{"id":2503,"depth":232,"text":2504},{"id":2947,"depth":211,"text":2947,"children":3399},[3400,3401,3403,3404],{"id":2950,"depth":232,"text":2951},{"id":3017,"depth":232,"text":3402},"坑 2：render.photo 是 (props, context) 双参签名",{"id":3220,"depth":232,"text":3221},{"id":3355,"depth":232,"text":3356},{"id":3373,"depth":211,"text":3373},"记录在 Magic Img 生图图库中引入 react-photo-album 实现 Masonry 瀑布流的过程：技术选型背景、需求梳理、安装使用，以及「忘记引入样式表导致一张一列」等踩坑与排查方法。","\u002Fposts\u002Freact-photo-album-masonry\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Freact-photo-album-masonry",{"title":2134,"description":3406},"前端工程实践","posts\u002Freact-photo-album-masonry\u002Findex",[2127,2225,3414,3415,3416],"瀑布流","前端","Tailwind","-O-kAQLEyRzwVhOpuo-yBxZim9LtvhChPkpP92ipyjM",{"id":3419,"title":3420,"author":7,"body":3421,"description":4594,"difficulty":451,"draft":452,"extension":453,"image":4595,"meta":4596,"navigation":394,"path":4597,"pinned":452,"published":4598,"seo":4599,"series":7,"seriesOrder":7,"stem":4600,"tags":4601,"updated":2130,"__hash__":4605},"posts\u002Fposts\u002Fip-geolocation-guide\u002Findex.md","IP 属地是怎么查出来的？本地查询与在线查询方案对比",{"type":9,"value":3422,"toc":4570},[3423,3426,3432,3435,3446,3455,3459,3465,3478,3486,3497,3504,3507,3514,3518,3529,3533,3544,3547,3551,3557,3563,3566,3570,3577,3595,3598,3601,3605,3636,3677,3683,3687,3830,3866,3875,3879,3919,3924,3928,3939,3942,3976,3979,3983,3986,3990,4185,4189,4271,4275,4282,4389,4393,4402,4488,4491,4494,4530,4533,4535,4543,4564,4567],[12,3424,3425],{"id":3425},"写在前面",[17,3427,3428,3429],{},"很多网站会在「登录记录」「安全中心」里显示你的登录 IP 归属地，比如「福建·福州 · 中国移动」。你可能好奇过：",[21,3430,3431],{},"服务器是怎么根据一个 IP 就判断出我在哪个城市、哪个运营商的？",[17,3433,3434],{},"这篇科普文章讲清楚三件事：",[509,3436,3437,3440,3443],{},[124,3438,3439],{},"IP 属地到底查的是什么；",[124,3441,3442],{},"纯本地查询为什么能做到毫秒级且不联网；",[124,3444,3445],{},"几种主流查询方案的优缺点对比，方便你按场景选型。",[1836,3447,3448],{},[17,3449,3450,3451,3454],{},"说明：文中用到的 IP 均为 RFC 5737 保留的文档示例段（TEST-NET-2，",[198,3452,3453],{},"198.51.100.0\u002F24","），不会对应任何真实地址。",[12,3456,3458],{"id":3457},"先想清楚ip-属地查的是什么","先想清楚：IP 属地查的是什么",[17,3460,3461,3462,1739],{},"一个常见误区是「每个 IP 对应一个具体地址」。实际上 IP 属地的本质是 ",[21,3463,3464],{},"IP 段与归属地的映射",[17,3466,3467,3468,578],{},"运营商和机构在申请 IP 时，是按",[21,3469,3470,3471,3477],{},"网段（CIDR）",[21,3472,3473,3474,3476],{},"成块分配的。比如 ",[198,3475,3453],{}," 这一个段，一整块都归属同一地区、同一运营商。所以归属地数据天然可以","按段存储",[191,3479,3484],{"className":3480,"code":3482,"language":3483,"meta":196},[3481],"language-text","IP 起始          IP 结束         归属地\n────────────────────────────────────────\n198.51.100.0     198.51.100.255  中国|福建|福州|移动\n198.51.101.0     198.51.101.255  中国|浙江|杭州|电信\n","text",[198,3485,3482],{"__ignoreMap":196},[17,3487,3488,3489,3492,3493,3496],{},"全球公网 IP 约有 42 亿个，但",[21,3490,3491],{},"网段只有几十万条","。正因为存的是「段」而不是「每个 IP」，完整的归属地数据文件才只有几 MB（ip2region 的 ",[198,3494,3495],{},".xdb"," 约 11MB）。这是「本地离线查询」可行的第一前提。",[17,3498,3499,3500,3503],{},"所以，",[21,3501,3502],{},"IP 属地从来不是精确到街道的定位","，它返回的是段级的「省｜市｜运营商」粗粒度信息。",[12,3505,3506],{"id":3506},"纯本地查询的原理",[17,3508,3509,3510,3513],{},"以开源的 ",[21,3511,3512],{},"ip2region"," 为例，它的查询完全在本地完成，不联网、不请求第三方，几十微秒到毫秒级返回。",[564,3515,3517],{"id":3516},"第一步把-ip-变成整数","第一步：把 IP 变成整数",[17,3519,3520,3521,3524,3525,3528],{},"以 ",[198,3522,3523],{},"198.51.100.42"," 为例，它会转成一个 32 位整数 ",[198,3526,3527],{},"0xC633642A","。IP 的四个数字本质就是 4 个字节，拼起来就是一个整数，这一步没有任何玄学。",[564,3530,3532],{"id":3531},"第二步二分查找","第二步：二分查找",[17,3534,3535,3536,3539,3540,3543],{},"在按起始 IP 排序的段列表里做",[21,3537,3538],{},"二分查找","，找到满足 ",[198,3541,3542],{},"段的起始IP ≤ 目标整数 ≤ 段的结束IP"," 的那一段。",[17,3545,3546],{},"二分查找的复杂度是 O(log N)，N 是几十万条，所以很快——这就是「不靠网络也能毫秒返回」的核心。",[564,3548,3550],{"id":3549},"第三步xdb-文件的两级结构","第三步：.xdb 文件的两级结构",[17,3552,3553,3554,3556],{},"ip2region 的 ",[198,3555,3495],{}," 不是简单顺序文件，内部分两个区，进一步加速：",[191,3558,3561],{"className":3559,"code":3560,"language":3483,"meta":196},[3481],"┌─────────────────────────────┐\n│  索引区 (Index)             │  \u002F\u002F 每段一条定长记录：startIP \u002F endIP \u002F 数据区指针 + 数据长度\n│  …… 二分快速定位段 ……       │\n├─────────────────────────────┤\n│  数据区 (Data)              │  \u002F\u002F 真正存放各段的地点字符串\n│  ……                         │\n└─────────────────────────────┘\n",[198,3562,3560],{"__ignoreMap":196},[17,3564,3565],{},"先二分索引区定位到「是哪一段」，再按指针去数据区读出地点字符串，一次磁盘读就能拿到结果。",[564,3567,3569],{"id":3568},"为什么离线数据不会很快过期","为什么「离线数据」不会很快过期",[17,3571,3572,3573,3576],{},"IP 归属取决于",[21,3574,3575],{},"注册机构和运营商的分配","，一个网段通常长期稳定属于某地区、某运营商。所以：",[121,3578,3579,3582,3592],{},[124,3580,3581],{},"下载一次数据文件，可以用很久；",[124,3583,3584,3585,3591],{},"归属偶尔变化时，",[21,3586,3587,3588,3590],{},"每月更新一次 ",[198,3589,3495],{}," 文件","即可（替换文件、不用重启服务，见下文实战）；",[124,3593,3594],{},"数据源来自 IANA\u002FAPNIC 等机构的分配表与公开归属信息，不是实时探测。",[12,3596,3597],{"id":3597},"主流查询方案对比",[17,3599,3600],{},"实际开发中，查询 IP 属地主要有三条路线。",[564,3602,3604],{"id":3603},"方案一ip2region纯本地离线推荐","方案一：ip2region（纯本地离线，推荐）",[191,3606,3608],{"className":757,"code":3607,"language":759,"meta":196,"style":196},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\nregion, err := searcher.SearchByStr(ip)\n\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[198,3609,3610,3615,3631],{"__ignoreMap":196},[201,3611,3612],{"class":203,"line":204},[201,3613,3614],{"class":207},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\n",[201,3616,3617,3620,3622,3625,3628],{"class":203,"line":211},[201,3618,3619],{"class":228},"region, err ",[201,3621,1214],{"class":214},[201,3623,3624],{"class":228}," searcher.",[201,3626,3627],{"class":291},"SearchByStr",[201,3629,3630],{"class":228},"(ip)\n",[201,3632,3633],{"class":203,"line":232},[201,3634,3635],{"class":207},"\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[35,3637,3638,3648],{},[38,3639,3640],{},[41,3641,3642,3645],{},[44,3643,3644],{},"优点",[44,3646,3647],{},"缺点",[51,3649,3650,3658,3666],{},[41,3651,3652,3655],{},[56,3653,3654],{},"免费、无网络依赖、无 QPS 限制",[56,3656,3657],{},"段级粒度，个别段可能显示到相邻地区",[41,3659,3660,3663],{},[56,3661,3662],{},"毫秒级查询，永不因外部服务挂掉",[56,3664,3665],{},"数据按月更新，最新分配的段可能短暂缺失",[41,3667,3668,3671],{},[56,3669,3670],{},"数据文件可随版本更新，不重启",[56,3672,3673,3674],{},"只返回省市区+运营商，无经纬度；",[21,3675,3676],{},"仅支持 IPv4",[17,3678,3679,3682],{},[21,3680,3681],{},"适用","：博客\u002F后台的「登录记录显示归属地」这类展示用途。",[564,3684,3686],{"id":3685},"方案二在线-apiip-api-高德-腾讯位置服务","方案二：在线 API（ip-api \u002F 高德 \u002F 腾讯位置服务）",[191,3688,3690],{"className":757,"code":3689,"language":759,"meta":196,"style":196},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\ndefer cancel()\nreq, _ := http.NewRequestWithContext(ctx, \"GET\", \"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\", nil)\nresp, err := http.DefaultClient.Do(req)\nif err != nil {\n    return \"未知\" \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n}\ndefer resp.Body.Close()\n\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[198,3691,3692,3697,3727,3737,3767,3783,3799,3809,3813,3825],{"__ignoreMap":196},[201,3693,3694],{"class":203,"line":204},[201,3695,3696],{"class":207},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\n",[201,3698,3699,3702,3704,3707,3710,3713,3716,3719,3722,3724],{"class":203,"line":211},[201,3700,3701],{"class":228},"ctx, cancel ",[201,3703,1214],{"class":214},[201,3705,3706],{"class":228}," context.",[201,3708,3709],{"class":291},"WithTimeout",[201,3711,3712],{"class":228},"(context.",[201,3714,3715],{"class":291},"Background",[201,3717,3718],{"class":228},"(), ",[201,3720,3721],{"class":221},"3",[201,3723,937],{"class":214},[201,3725,3726],{"class":228},"time.Second)\n",[201,3728,3729,3732,3735],{"class":203,"line":232},[201,3730,3731],{"class":214},"defer",[201,3733,3734],{"class":291}," cancel",[201,3736,1223],{"class":228},[201,3738,3739,3742,3744,3746,3749,3752,3755,3757,3760,3762,3765],{"class":203,"line":245},[201,3740,3741],{"class":228},"req, _ ",[201,3743,1214],{"class":214},[201,3745,1090],{"class":228},[201,3747,3748],{"class":291},"NewRequestWithContext",[201,3750,3751],{"class":228},"(ctx, ",[201,3753,3754],{"class":238},"\"GET\"",[201,3756,338],{"class":228},[201,3758,3759],{"class":238},"\"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\"",[201,3761,338],{"class":228},[201,3763,3764],{"class":221},"nil",[201,3766,822],{"class":228},[201,3768,3769,3772,3774,3777,3780],{"class":203,"line":256},[201,3770,3771],{"class":228},"resp, err ",[201,3773,1214],{"class":214},[201,3775,3776],{"class":228}," http.DefaultClient.",[201,3778,3779],{"class":291},"Do",[201,3781,3782],{"class":228},"(req)\n",[201,3784,3785,3788,3791,3794,3797],{"class":203,"line":267},[201,3786,3787],{"class":214},"if",[201,3789,3790],{"class":228}," err ",[201,3792,3793],{"class":214},"!=",[201,3795,3796],{"class":221}," nil",[201,3798,229],{"class":228},[201,3800,3801,3803,3806],{"class":203,"line":273},[201,3802,1087],{"class":214},[201,3804,3805],{"class":238}," \"未知\"",[201,3807,3808],{"class":207}," \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n",[201,3810,3811],{"class":203,"line":819},[201,3812,276],{"class":228},[201,3814,3815,3817,3820,3823],{"class":203,"line":825},[201,3816,3731],{"class":214},[201,3818,3819],{"class":228}," resp.Body.",[201,3821,3822],{"class":291},"Close",[201,3824,1223],{"class":228},[201,3826,3827],{"class":203,"line":830},[201,3828,3829],{"class":207},"\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[35,3831,3832,3840],{},[38,3833,3834],{},[41,3835,3836,3838],{},[44,3837,3644],{},[44,3839,3647],{},[51,3841,3842,3850,3858],{},[41,3843,3844,3847],{},[56,3845,3846],{},"数据最新、可能更精确",[56,3848,3849],{},"有频率限制（免费版限 QPS）",[41,3851,3852,3855],{},[56,3853,3854],{},"部分服务还返回经纬度\u002F时区",[56,3856,3857],{},"依赖第三方，断网\u002F被墙\u002F限流时拿不到",[41,3859,3860,3863],{},[56,3861,3862],{},"无需维护数据文件",[56,3864,3865],{},"每次查询都要发一次 HTTP 请求",[17,3867,3868,3870,3871,3874],{},[21,3869,3681],{},"：对精度要求高、且能接受外部依赖与限流的场景。",[21,3872,3873],{},"务必加超时和降级","，否则第三方一抖，你自己的页面就卡住。",[564,3876,3878],{"id":3877},"方案三maxmind-geolite2离线-mmdb","方案三：MaxMind GeoLite2（离线 .mmdb）",[35,3880,3881,3889],{},[38,3882,3883],{},[41,3884,3885,3887],{},[44,3886,3644],{},[44,3888,3647],{},[51,3890,3891,3899,3911],{},[41,3892,3893,3896],{},[56,3894,3895],{},"全球数据，覆盖广、精度较好",[56,3897,3898],{},"免费但需注册账号并遵守 EULA；商用需 GeoIP2 商业授权",[41,3900,3901,3908],{},[56,3902,3903,3904,3907],{},"标准 ",[198,3905,3906],{},".mmdb"," 格式，各语言都有库，支持 IPv6",[56,3909,3910],{},"国内 IP 的省市粒度有时不如 ip2region 细",[41,3912,3913,3916],{},[56,3914,3915],{},"离线查询",[56,3917,3918],{},"数据同样要定期更新",[17,3920,3921,3923],{},[21,3922,3681],{},"：面向全球用户的场景。",[12,3925,3927],{"id":3926},"ipv6本地离线方案的一个盲点","IPv6：本地离线方案的一个盲点",[17,3929,3930,3931,3934,3935,3938],{},"前面聊的都是 IPv4（32 位）。但家宽和移动网络越来越多地",[21,3932,3933],{},"只下发 IPv6","（128 位）地址，而 ",[21,3936,3937],{},"ip2region 目前只支持 IPv4","——拿到一个纯 IPv6 地址是查不出归属地的，本地库会返回空结果。",[17,3940,3941],{},"实际影响与常见应对：",[509,3943,3944,3954,3960,3970],{},[124,3945,3946,3949,3950,3953],{},[21,3947,3948],{},"判断版本","：Go 里 ",[198,3951,3952],{},"net.ParseIP(ip).To4() != nil"," 就是 IPv4，先判断再查询；",[124,3955,3956,3959],{},[21,3957,3958],{},"降级展示","：IPv6 直接显示「IPv6 地址」，不显示属地，诚实第一；",[124,3961,3962,3965,3966,3969],{},[21,3963,3964],{},"优先取 IPv4","：有反向代理时，从 ",[198,3967,3968],{},"X-Forwarded-For"," 里挑一个 IPv4 来查；",[124,3971,3972,3975],{},[21,3973,3974],{},"需要 v6 归属","：换支持 IPv6 的数据源（如 MaxMind GeoLite2）。",[17,3977,3978],{},"这一点在接入时很容易漏掉，上线前记得拿一个纯 IPv6 地址回归一遍。",[12,3980,3982],{"id":3981},"实战给登录记录加-ip-属地","实战：给登录记录加 IP 属地",[17,3984,3985],{},"subLog 博客的登录记录就用了 ip2region。完整接入一共四步。",[564,3987,3989],{"id":3988},"_1-进程启动时加载数据文件","1. 进程启动时加载数据文件",[191,3991,3993],{"className":757,"code":3992,"language":759,"meta":196,"style":196},"import (\n    \"sync\u002Fatomic\"\n    \"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb\"\n)\n\n\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\nvar searcher atomic.Pointer[xdb.Searcher]\n\nfunc initSearcher() error {\n    content, err := os.ReadFile(\".\u002Fdata\u002Fip2region.xdb\") \u002F\u002F 约 11MB，一次性读入内存\n    if err != nil {\n        return err\n    }\n    s, err := xdb.NewWithBuffer(content)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s)\n    return nil\n}\n",[198,3994,3995,4001,4010,4019,4023,4027,4032,4060,4064,4078,4101,4113,4121,4125,4141,4153,4159,4163,4174,4181],{"__ignoreMap":196},[201,3996,3997,3999],{"class":203,"line":204},[201,3998,784],{"class":214},[201,4000,787],{"class":228},[201,4002,4003,4005,4008],{"class":203,"line":211},[201,4004,792],{"class":238},[201,4006,4007],{"class":291},"sync\u002Fatomic",[201,4009,798],{"class":238},[201,4011,4012,4014,4017],{"class":203,"line":232},[201,4013,792],{"class":238},[201,4015,4016],{"class":291},"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb",[201,4018,798],{"class":238},[201,4020,4021],{"class":203,"line":245},[201,4022,822],{"class":228},[201,4024,4025],{"class":203,"line":256},[201,4026,779],{"emptyLinePlaceholder":394},[201,4028,4029],{"class":203,"line":267},[201,4030,4031],{"class":207},"\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\n",[201,4033,4034,4036,4039,4042,4044,4047,4050,4053,4055,4058],{"class":203,"line":273},[201,4035,839],{"class":214},[201,4037,4038],{"class":228}," searcher ",[201,4040,4041],{"class":291},"atomic",[201,4043,847],{"class":228},[201,4045,4046],{"class":291},"Pointer",[201,4048,4049],{"class":228},"[",[201,4051,4052],{"class":291},"xdb",[201,4054,847],{"class":228},[201,4056,4057],{"class":291},"Searcher",[201,4059,344],{"class":228},[201,4061,4062],{"class":203,"line":819},[201,4063,779],{"emptyLinePlaceholder":394},[201,4065,4066,4068,4071,4074,4076],{"class":203,"line":825},[201,4067,959],{"class":214},[201,4069,4070],{"class":291}," initSearcher",[201,4072,4073],{"class":228},"() ",[201,4075,1000],{"class":214},[201,4077,229],{"class":228},[201,4079,4080,4083,4085,4088,4091,4093,4096,4098],{"class":203,"line":830},[201,4081,4082],{"class":228},"    content, err ",[201,4084,1214],{"class":214},[201,4086,4087],{"class":228}," os.",[201,4089,4090],{"class":291},"ReadFile",[201,4092,979],{"class":228},[201,4094,4095],{"class":238},"\".\u002Fdata\u002Fip2region.xdb\"",[201,4097,973],{"class":228},[201,4099,4100],{"class":207},"\u002F\u002F 约 11MB，一次性读入内存\n",[201,4102,4103,4105,4107,4109,4111],{"class":203,"line":836},[201,4104,1009],{"class":214},[201,4106,3790],{"class":228},[201,4108,3793],{"class":214},[201,4110,3796],{"class":221},[201,4112,229],{"class":228},[201,4114,4115,4118],{"class":203,"line":853},[201,4116,4117],{"class":214},"        return",[201,4119,4120],{"class":228}," err\n",[201,4122,4123],{"class":203,"line":858},[201,4124,1081],{"class":228},[201,4126,4127,4130,4132,4135,4138],{"class":203,"line":864},[201,4128,4129],{"class":228},"    s, err ",[201,4131,1214],{"class":214},[201,4133,4134],{"class":228}," xdb.",[201,4136,4137],{"class":291},"NewWithBuffer",[201,4139,4140],{"class":228},"(content)\n",[201,4142,4143,4145,4147,4149,4151],{"class":203,"line":903},[201,4144,1009],{"class":214},[201,4146,3790],{"class":228},[201,4148,3793],{"class":214},[201,4150,3796],{"class":221},[201,4152,229],{"class":228},[201,4154,4155,4157],{"class":203,"line":908},[201,4156,4117],{"class":214},[201,4158,4120],{"class":228},[201,4160,4161],{"class":203,"line":922},[201,4162,1081],{"class":228},[201,4164,4165,4168,4171],{"class":203,"line":931},[201,4166,4167],{"class":228},"    searcher.",[201,4169,4170],{"class":291},"Store",[201,4172,4173],{"class":228},"(s)\n",[201,4175,4176,4178],{"class":203,"line":946},[201,4177,1087],{"class":214},[201,4179,4180],{"class":221}," nil\n",[201,4182,4183],{"class":203,"line":951},[201,4184,276],{"class":228},[564,4186,4188],{"id":4187},"_2-查询一次","2. 查询一次",[191,4190,4192],{"className":757,"code":4191,"language":759,"meta":196,"style":196},"func ipRegion(ip string) string {\n    region, err := searcher.Load().SearchByStr(ip)\n    if err != nil {\n        return \"未知\"\n    }\n    return region \u002F\u002F \"中国|福建省|福州市|移动\"\n}\n",[198,4193,4194,4215,4234,4246,4253,4257,4267],{"__ignoreMap":196},[201,4195,4196,4198,4201,4203,4206,4208,4210,4213],{"class":203,"line":204},[201,4197,959],{"class":214},[201,4199,4200],{"class":291}," ipRegion",[201,4202,979],{"class":228},[201,4204,4205],{"class":965},"ip",[201,4207,985],{"class":214},[201,4209,973],{"class":228},[201,4211,4212],{"class":214},"string",[201,4214,229],{"class":228},[201,4216,4217,4220,4222,4224,4227,4230,4232],{"class":203,"line":211},[201,4218,4219],{"class":228},"    region, err ",[201,4221,1214],{"class":214},[201,4223,3624],{"class":228},[201,4225,4226],{"class":291},"Load",[201,4228,4229],{"class":228},"().",[201,4231,3627],{"class":291},[201,4233,3630],{"class":228},[201,4235,4236,4238,4240,4242,4244],{"class":203,"line":232},[201,4237,1009],{"class":214},[201,4239,3790],{"class":228},[201,4241,3793],{"class":214},[201,4243,3796],{"class":221},[201,4245,229],{"class":228},[201,4247,4248,4250],{"class":203,"line":245},[201,4249,4117],{"class":214},[201,4251,4252],{"class":238}," \"未知\"\n",[201,4254,4255],{"class":203,"line":256},[201,4256,1081],{"class":228},[201,4258,4259,4261,4264],{"class":203,"line":267},[201,4260,1087],{"class":214},[201,4262,4263],{"class":228}," region ",[201,4265,4266],{"class":207},"\u002F\u002F \"中国|福建省|福州市|移动\"\n",[201,4268,4269],{"class":203,"line":273},[201,4270,276],{"class":228},[564,4272,4274],{"id":4273},"_3-加一层内存缓存","3. 加一层内存缓存",[17,4276,4277,4278,4281],{},"登录记录里同一个 IP 可能反复出现，用 ",[198,4279,4280],{},"sync.Map"," 按 IP 缓存结果，一次查询终身复用：",[191,4283,4285],{"className":757,"code":4284,"language":759,"meta":196,"style":196},"var regionCache sync.Map\n\nfunc ipRegionCached(ip string) string {\n    if v, ok := regionCache.Load(ip); ok {\n        return v.(string)\n    }\n    region := ipRegion(ip)\n    regionCache.Store(ip, region)\n    return region\n}\n",[198,4286,4287,4302,4306,4325,4342,4353,4357,4368,4378,4385],{"__ignoreMap":196},[201,4288,4289,4291,4294,4297,4299],{"class":203,"line":204},[201,4290,839],{"class":214},[201,4292,4293],{"class":228}," regionCache ",[201,4295,4296],{"class":291},"sync",[201,4298,847],{"class":228},[201,4300,4301],{"class":291},"Map\n",[201,4303,4304],{"class":203,"line":211},[201,4305,779],{"emptyLinePlaceholder":394},[201,4307,4308,4310,4313,4315,4317,4319,4321,4323],{"class":203,"line":232},[201,4309,959],{"class":214},[201,4311,4312],{"class":291}," ipRegionCached",[201,4314,979],{"class":228},[201,4316,4205],{"class":965},[201,4318,985],{"class":214},[201,4320,973],{"class":228},[201,4322,4212],{"class":214},[201,4324,229],{"class":228},[201,4326,4327,4329,4332,4334,4337,4339],{"class":203,"line":245},[201,4328,1009],{"class":214},[201,4330,4331],{"class":228}," v, ok ",[201,4333,1214],{"class":214},[201,4335,4336],{"class":228}," regionCache.",[201,4338,4226],{"class":291},[201,4340,4341],{"class":228},"(ip); ok {\n",[201,4343,4344,4346,4349,4351],{"class":203,"line":256},[201,4345,4117],{"class":214},[201,4347,4348],{"class":228}," v.(",[201,4350,4212],{"class":214},[201,4352,822],{"class":228},[201,4354,4355],{"class":203,"line":267},[201,4356,1081],{"class":228},[201,4358,4359,4362,4364,4366],{"class":203,"line":273},[201,4360,4361],{"class":228},"    region ",[201,4363,1214],{"class":214},[201,4365,4200],{"class":291},[201,4367,3630],{"class":228},[201,4369,4370,4373,4375],{"class":203,"line":819},[201,4371,4372],{"class":228},"    regionCache.",[201,4374,4170],{"class":291},[201,4376,4377],{"class":228},"(ip, region)\n",[201,4379,4380,4382],{"class":203,"line":825},[201,4381,1087],{"class":214},[201,4383,4384],{"class":228}," region\n",[201,4386,4387],{"class":203,"line":830},[201,4388,276],{"class":228},[564,4390,4392],{"id":4391},"_4-数据热更新不用重启","4. 数据热更新（不用重启）",[17,4394,4395,4396,4398,4399,4401],{},"ip2region 每月发布新数据。下载新文件后重新 ",[198,4397,4137],{}," 并 ",[198,4400,4170],{}," 覆盖即可，进程完全不用重启：",[191,4403,4405],{"className":757,"code":4404,"language":759,"meta":196,"style":196},"func reloadSearcher(newContent []byte) error {\n    s, err := xdb.NewWithBuffer(newContent)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s) \u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n    return nil\n}\n",[198,4406,4407,4431,4444,4456,4462,4466,4478,4484],{"__ignoreMap":196},[201,4408,4409,4411,4414,4416,4419,4422,4425,4427,4429],{"class":203,"line":204},[201,4410,959],{"class":214},[201,4412,4413],{"class":291}," reloadSearcher",[201,4415,979],{"class":228},[201,4417,4418],{"class":965},"newContent",[201,4420,4421],{"class":228}," []",[201,4423,4424],{"class":214},"byte",[201,4426,973],{"class":228},[201,4428,1000],{"class":214},[201,4430,229],{"class":228},[201,4432,4433,4435,4437,4439,4441],{"class":203,"line":211},[201,4434,4129],{"class":228},[201,4436,1214],{"class":214},[201,4438,4134],{"class":228},[201,4440,4137],{"class":291},[201,4442,4443],{"class":228},"(newContent)\n",[201,4445,4446,4448,4450,4452,4454],{"class":203,"line":232},[201,4447,1009],{"class":214},[201,4449,3790],{"class":228},[201,4451,3793],{"class":214},[201,4453,3796],{"class":221},[201,4455,229],{"class":228},[201,4457,4458,4460],{"class":203,"line":245},[201,4459,4117],{"class":214},[201,4461,4120],{"class":228},[201,4463,4464],{"class":203,"line":256},[201,4465,1081],{"class":228},[201,4467,4468,4470,4472,4475],{"class":203,"line":267},[201,4469,4167],{"class":228},[201,4471,4170],{"class":291},[201,4473,4474],{"class":228},"(s) ",[201,4476,4477],{"class":207},"\u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n",[201,4479,4480,4482],{"class":203,"line":273},[201,4481,1087],{"class":214},[201,4483,4180],{"class":221},[201,4485,4486],{"class":203,"line":819},[201,4487,276],{"class":228},[12,4489,4490],{"id":4490},"局限与诚实提醒",[17,4492,4493],{},"无论用哪种方案，都要清楚 IP 属地的边界：",[121,4495,4496,4502,4508,4514,4520],{},[124,4497,4498,4501],{},[21,4499,4500],{},"段级精度","：网段归属是「绝大多数」准确，个别 IP（大型企业自建段、跨省调度段）可能显示到相邻地区或只到省。",[124,4503,4504,4507],{},[21,4505,4506],{},"运营商级，不是定位","：拿到的是「省｜市｜运营商」，不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位，纯 IP 归属做不到。",[124,4509,4510,4513],{},[21,4511,4512],{},"动态 IP","：家宽通常是动态 IP，重拨会变，所以「上次登录在福州，这次在广州」不一定是异常，可能是 IP 变了。",[124,4515,4516,4519],{},[21,4517,4518],{},"别用于安全判定","：用 IP 段做封禁或风控判定风险自负，段级数据不适合当精确依据。",[124,4521,4522,4525,4526,4529],{},[21,4523,4524],{},"隐私与合规","：IP 地址属于个人信息（《个人信息保护法》）。归属地展示建议只出现在",[21,4527,4528],{},"用户本人可见","的页面（登录记录、个人中心）；若在评论区等公开场合展示，建议只显示到省市并明确告知用户。",[12,4531,4532],{"id":4532},"小结",[17,4534,3382],{},[1836,4536,4537],{},[17,4538,4539,4542],{},[21,4540,4541],{},"IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。"," 它查的是网段归属，不是精确位置。",[121,4544,4545,4551,4558],{},[124,4546,4547,4548,4550],{},"想省心、免费、稳定，用 ",[21,4549,3512],{}," 本地离线查询（注意 IPv6 盲点）；",[124,4552,4553,4554,4557],{},"想要最新最细、能接受外部依赖，用",[21,4555,4556],{},"在线 API","（记得超时降级）；",[124,4559,4560,4561,1739],{},"面向全球用户、要 IPv6 覆盖，用 ",[21,4562,4563],{},"MaxMind GeoLite2",[17,4565,4566],{},"对个人博客「登录记录显示归属地」这种场景，本地离线方案通常是性价比最高的选择。",[438,4568,4569],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":196,"searchDepth":211,"depth":232,"links":4571},[4572,4573,4574,4580,4585,4586,4592,4593],{"id":3425,"depth":211,"text":3425},{"id":3457,"depth":211,"text":3458},{"id":3506,"depth":211,"text":3506,"children":4575},[4576,4577,4578,4579],{"id":3516,"depth":232,"text":3517},{"id":3531,"depth":232,"text":3532},{"id":3549,"depth":232,"text":3550},{"id":3568,"depth":232,"text":3569},{"id":3597,"depth":211,"text":3597,"children":4581},[4582,4583,4584],{"id":3603,"depth":232,"text":3604},{"id":3685,"depth":232,"text":3686},{"id":3877,"depth":232,"text":3878},{"id":3926,"depth":211,"text":3927},{"id":3981,"depth":211,"text":3982,"children":4587},[4588,4589,4590,4591],{"id":3988,"depth":232,"text":3989},{"id":4187,"depth":232,"text":4188},{"id":4273,"depth":232,"text":4274},{"id":4391,"depth":232,"text":4392},{"id":4490,"depth":211,"text":4490},{"id":4532,"depth":211,"text":4532},"科普 IP 属地查询的原理：IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级，对比 ip2region、在线 API 与 MaxMind 三种方案的优劣，并附登录记录场景的 Go 实战与 IPv6 盲点提醒。","\u002Fposts\u002Fip-geolocation-guide\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fip-geolocation-guide","2026-08-07T12:00:00",{"title":3420,"description":4594},"posts\u002Fip-geolocation-guide\u002Findex",[4602,4603,4604],"IP","网络","科普","RY1UPgJaiKsjpC51uqMyG7w_GpCRS13_CX5j1hDD_dM",{"id":4607,"title":4608,"author":7,"body":4609,"description":5158,"difficulty":451,"draft":452,"extension":453,"image":5159,"meta":5160,"navigation":394,"path":5161,"pinned":452,"published":5162,"seo":5163,"series":5164,"seriesOrder":204,"stem":5165,"tags":5166,"updated":5168,"__hash__":5169},"posts\u002Fposts\u002Fsublog-evolution\u002Findex.md","尼克的小窝 架构变迁：从纯静态到前后端分离",{"type":9,"value":4610,"toc":5138},[4611,4614,4617,4628,4632,4636,4642,4646,4678,4681,4695,4699,4702,4771,4775,4778,4784,4788,4799,4829,4832,4836,4845,4848,4892,4896,5019,5023,5030,5034,5040,5046,5050,5053,5104,5108,5115,5122,5128,5131,5133],[12,4612,4613],{"id":3425},"📝 写在前面",[17,4615,4616],{},"SuBlog 最初只是一个简单的技术博客——Nuxt 3 静态生成，Markdown 写文章，部署到静态托管完事。但随着功能需求的增长，纯静态的架构开始显露出局限性。",[17,4618,4619,4620,4623,4624,4627],{},"这篇文章记录从 ",[21,4621,4622],{},"纯静态"," 到 ",[21,4625,4626],{},"前后端分离"," 的演进过程，带你了解每一步背后的思考。",[12,4629,4631],{"id":4630},"第一代纯静态nuxt-3-nuxtcontent","📦 第一代：纯静态（Nuxt 3 + @nuxt\u002Fcontent）",[564,4633,4635],{"id":4634},"️-架构图","🏗️ 架构图",[191,4637,4640],{"className":4638,"code":4639,"language":3483,"meta":196},[3481],"┌─────────────────────────────────────┐\n│          静态托管 (Nginx\u002FVercel)      │\n│                                     │\n│  ┌───────────────────────────────┐  │\n│  │     Nuxt 3 SSG 产物            │  │\n│  │                               │  │\n│  │  pages\u002F + content\u002F + public\u002F  │  │\n│  └───────────────────────────────┘  │\n│                                     │\n│  Giscus (GitHub Discussions 评论)    │\n└─────────────────────────────────────┘\n",[198,4641,4639],{"__ignoreMap":196},[564,4643,4645],{"id":4644},"做了什么","🔧 做了什么",[121,4647,4648,4658,4663,4668],{},[124,4649,4650,4652,4653,4652,4655,4657],{},[21,4651,60],{}," + ",[21,4654,70],{},[21,4656,80],{}," 搭建前端",[124,4659,4660,4662],{},[21,4661,90],{}," 管理 Markdown 文章，编译时生成静态页面",[124,4664,4665,4667],{},[21,4666,110],{}," 嵌入评论，依赖 GitHub Discussions",[124,4669,4670,4671,4674,4675,4677],{},"部署通过 ",[198,4672,4673],{},"nuxt generate"," 输出 ",[198,4676,378],{},"，丢到 Nginx 或 Vercel",[564,4679,4680],{"id":3644},"✅ 优点",[121,4682,4683,4689,4692],{},[124,4684,4685,4686,4688],{},"部署极简，一个 ",[198,4687,4673],{}," 搞定",[124,4690,4691],{},"加载飞快，全是 HTML，没有服务端开销",[124,4693,4694],{},"无需后端，零运维成本",[564,4696,4698],{"id":4697},"局限","❌ 局限",[17,4700,4701],{},"随着时间的推移，纯静态的问题越来越明显：",[35,4703,4704,4714],{},[38,4705,4706],{},[41,4707,4708,4711],{},[44,4709,4710],{},"问题",[44,4712,4713],{},"具体表现",[51,4715,4716,4727,4738,4749,4760],{},[41,4717,4718,4724],{},[56,4719,4720,4721],{},"🐢 ",[21,4722,4723],{},"内容更新慢",[56,4725,4726],{},"改一篇文章要重新构建、重新部署，不能实时发布",[41,4728,4729,4735],{},[56,4730,4731,4732],{},"👤 ",[21,4733,4734],{},"没有用户体系",[56,4736,4737],{},"无法区分读者和管理员，所有内容对所有人一致可见",[41,4739,4740,4746],{},[56,4741,4742,4743],{},"🔒 ",[21,4744,4745],{},"评论受限",[56,4747,4748],{},"Giscus 需要 GitHub 账号才能评论，门槛高",[41,4750,4751,4757],{},[56,4752,4753,4754],{},"🚫 ",[21,4755,4756],{},"无法做动态功能",[56,4758,4759],{},"点赞、收藏、阅读统计、个性化推荐……统统做不了",[41,4761,4762,4768],{},[56,4763,4764,4765],{},"🔗 ",[21,4766,4767],{},"前端锁死",[56,4769,4770],{},"被 Nuxt\u002FVue 生态绑定，想换技术栈得重写",[12,4772,4774],{"id":4773},"第二代前后端分离react-go","⚡ 第二代：前后端分离（React + Go）",[564,4776,4635],{"id":4777},"️-架构图-1",[191,4779,4782],{"className":4780,"code":4781,"language":3483,"meta":196},[3481],"┌───────────────────────┐       ┌────────────────────┐\n│    静态托管 (Vercel)    │       │   Go 服务端         │\n│                       │       │                    │\n│  ┌─────────────────┐  │       │  ┌──────────────┐  │\n│  │   sublog-react   │  │  API  │  │  HTTP API    │  │\n│  │  (Next.js SSG)  │──┼───────┼─>│              │  │\n│  │                  │  │       │  │ 文章 \u002F 用户  │  │\n│  │  首页 \u002F 文章 \u002F   │  │       │  └──────────────┘  │\n│  │  用户中心        │  │       │                    │\n│  └─────────────────┘  │       │  ┌──────────────┐  │\n│                       │       │  │   数据库      │  │\n└───────────────────────┘       │  └──────────────┘  │\n                                └────────────────────┘\n",[198,4783,4781],{"__ignoreMap":196},[564,4785,4787],{"id":4786},"️-前端sublog-react","🖥️ 前端：sublog-react",[17,4789,4790,4791,4794,4795,4798],{},"选用 ",[21,4792,4793],{},"Next.js","（App Router） + ",[21,4796,4797],{},"React 19","，理由是：",[121,4800,4801,4808,4815,4822],{},[124,4802,4803,4804,4807],{},"🎨 ",[21,4805,4806],{},"React 生态","——组件库成熟，开发效率高",[124,4809,4810,4811,4814],{},"📐 ",[21,4812,4813],{},"App Router","——支持布局嵌套，页面组织清晰",[124,4816,4817,4818,4821],{},"📦 ",[21,4819,4820],{},"静态导出","——仍可部署到静态托管",[124,4823,4824,4825,4828],{},"⚙️ ",[21,4826,4827],{},"运行时配置","——API 地址可动态注入，切换环境无需重新构建",[17,4830,4831],{},"前端不再直接读取 Markdown 文件，而是通过 HTTP API 获取文章数据。静态导出 + 客户端数据获取的组合，既保留了首屏加载速度，又实现了内容的动态更新。",[564,4833,4835],{"id":4834},"️-后端sublog-s","⚙️ 后端：sublog-s",[17,4837,4838,4839,4652,4841,4844],{},"后端使用 ",[21,4840,2126],{},[21,4842,4843],{},"Gin","，按业务逻辑分层组织代码。",[17,4846,4847],{},"核心技术选型：",[35,4849,4850,4858],{},[38,4851,4852],{},[41,4853,4854,4856],{},[44,4855,46],{},[44,4857,49],{},[51,4859,4860,4870,4881],{},[41,4861,4862,4867],{},[56,4863,4864,4865],{},"🚀 ",[21,4866,4843],{},[56,4868,4869],{},"HTTP 框架",[41,4871,4872,4878],{},[56,4873,4874,4875],{},"🗄️ ",[21,4876,4877],{},"GORM",[56,4879,4880],{},"ORM，数据库操作",[41,4882,4883,4889],{},[56,4884,4885,4886],{},"🔐 ",[21,4887,4888],{},"JWT",[56,4890,4891],{},"用户认证",[12,4893,4895],{"id":4894},"前后对比","📊 前后对比",[35,4897,4898,4911],{},[38,4899,4900],{},[41,4901,4902,4905,4908],{},[44,4903,4904],{},"维度",[44,4906,4907],{},"第一代（纯静态）",[44,4909,4910],{},"第二代（前后端分离）",[51,4912,4913,4926,4939,4953,4966,4979,4993,5005],{},[41,4914,4915,4920,4923],{},[56,4916,4803,4917],{},[21,4918,4919],{},"前端框架",[56,4921,4922],{},"Nuxt 3 (Vue)",[56,4924,4925],{},"Next.js (React 19)",[41,4927,4928,4934,4936],{},[56,4929,4930,4931],{},"🖥️ ",[21,4932,4933],{},"后端",[56,4935,2199],{},[56,4937,4938],{},"Go + Gin",[41,4940,4941,4947,4950],{},[56,4942,4943,4944],{},"📄 ",[21,4945,4946],{},"内容来源",[56,4948,4949],{},"Markdown 文件",[56,4951,4952],{},"API 数据库",[41,4954,4955,4960,4963],{},[56,4956,4864,4957],{},[21,4958,4959],{},"发布方式",[56,4961,4962],{},"全量重新构建",[56,4964,4965],{},"API 实时发布",[41,4967,4968,4973,4976],{},[56,4969,4731,4970],{},[21,4971,4972],{},"用户系统",[56,4974,4975],{},"❌ 无",[56,4977,4978],{},"✅ 注册\u002F登录",[41,4980,4981,4987,4990],{},[56,4982,4983,4984],{},"💬 ",[21,4985,4986],{},"评论",[56,4988,4989],{},"Giscus（需 GitHub）",[56,4991,4992],{},"自有评论系统（规划中）",[41,4994,4995,4999,5002],{},[56,4996,4817,4997],{},[21,4998,351],{},[56,5000,5001],{},"1 个静态目录",[56,5003,5004],{},"前端静态 + 后端二进制",[41,5006,5007,5013,5016],{},[56,5008,5009,5010],{},"⚡ ",[21,5011,5012],{},"更新体验",[56,5014,5015],{},"改文章 → 构建 → 部署",[56,5017,5018],{},"改文章 → 立即生效",[12,5020,5022],{"id":5021},"新功能注册账号","🎉 新功能：注册账号",[17,5024,5025,5026,5029],{},"伴随着架构升级，",[21,5027,5028],{},"用户账号系统","也上线了。",[564,5031,5033],{"id":5032},"目前支持","📋 目前支持",[17,5035,5036,5037,578],{},"现在你可以在尼克的小窝 ",[21,5038,5039],{},"注册一个账号",[191,5041,5044],{"className":5042,"code":5043,"language":3483},[3481],"1. ✏️ 填写用户名、邮箱、密码\n2. 📧 查收激活邮件\n3. 🔗 点击链接激活账号\n4. 🚪 登录后查看和管理个人信息\n",[198,5045,5043],{"__ignoreMap":196},[12,5047,5049],{"id":5048},"️-后续计划","🗓️ 后续计划",[17,5051,5052],{},"正在开发或规划中的功能：",[121,5054,5056,5062,5068,5074,5080,5086,5092,5098],{"className":5055},[386],[124,5057,5059,5061],{"className":5058},[390],[392,5060],{"checked":394,"disabled":394,"type":395}," 用户注册\u002F登录（已上线 🎉）",[124,5063,5065,5067],{"className":5064},[390],[392,5066],{"checked":394,"disabled":394,"type":395}," 个人资料编辑（已上线 🎉）",[124,5069,5071,5073],{"className":5070},[390],[392,5072],{"disabled":394,"type":395}," 💬 站内评论系统",[124,5075,5077,5079],{"className":5076},[390],[392,5078],{"disabled":394,"type":395}," 🔖 文章收藏",[124,5081,5083,5085],{"className":5082},[390],[392,5084],{"disabled":394,"type":395}," 📚 阅读历史",[124,5087,5089,5091],{"className":5088},[390],[392,5090],{"disabled":394,"type":395}," 👍 文章点赞",[124,5093,5095,5097],{"className":5094},[390],[392,5096],{"disabled":394,"type":395}," 🔔 关注通知",[124,5099,5101,5103],{"className":5100},[390],[392,5102],{"disabled":394,"type":395}," 🎨 个人首页定制",[12,5105,5107],{"id":5106},"结语","💡 结语",[17,5109,5110,5111,5114],{},"从 Nuxt 3 纯静态到 React + Go 前后端分离，SuBlog 的架构变迁本质上是一个",[21,5112,5113],{},"功能驱动","的过程。静态博客有它的位置——写写文章、展示内容完全够用。但当你想做的不仅仅是\"展示\"时，后端的能力就不可或缺了。",[17,5116,5117,5118,5121],{},"这次重构最大的感触是：",[21,5119,5120],{},"架构选型不是越复杂越好，而是刚好够用最好","。纯静态阶段用 Nuxt 3 一点问题没有，直到功能需求超出了静态的边界，才自然推动到前后端分离。",[17,5123,5124,5125],{},"如果你也在纠结\"要不要上后端\"，不妨先问问自己：",[21,5126,5127],{},"你要做的是\"展示内容\"还是\"提供服务\"？",[17,5129,5130],{},"前者，静态就够了；后者，来吧，后端欢迎你。😄",[430,5132],{},[17,5134,5135],{},[21,5136,5137],{},"🎉 目前注册功能已经可以用了，后续更多好玩的功能正在路上。部分功能可能需要登录才能体验，快注册一个账号吧！",{"title":196,"searchDepth":211,"depth":232,"links":5139},[5140,5141,5147,5152,5153,5156,5157],{"id":3425,"depth":211,"text":4613},{"id":4630,"depth":211,"text":4631,"children":5142},[5143,5144,5145,5146],{"id":4634,"depth":232,"text":4635},{"id":4644,"depth":232,"text":4645},{"id":3644,"depth":232,"text":4680},{"id":4697,"depth":232,"text":4698},{"id":4773,"depth":211,"text":4774,"children":5148},[5149,5150,5151],{"id":4777,"depth":232,"text":4635},{"id":4786,"depth":232,"text":4787},{"id":4834,"depth":232,"text":4835},{"id":4894,"depth":211,"text":4895},{"id":5021,"depth":211,"text":5022,"children":5154},[5155],{"id":5032,"depth":232,"text":5033},{"id":5048,"depth":211,"text":5049},{"id":5106,"depth":211,"text":5107},"记录尼克的小窝从 Nuxt 3 纯静态博客演进到 React + Go 前后端分离架构的全过程，包括架构设计的思考、技术选型的变化，以及新增的账号注册功能。","\u002Fposts\u002Fsublog-evolution\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsublog-evolution","2026-07-23T12:00:00",{"title":4608,"description":5158},"尼克的小窝 开发记录","posts\u002Fsublog-evolution\u002Findex",[5167,2127,4793,2126],"架构","2026-07-23","VMQ6A2K8GQ5t3DSlezWPd3KgXnmDVMn9QVeaI28Hcp8",{"id":5171,"title":5172,"author":7,"body":5173,"description":6584,"difficulty":451,"draft":452,"extension":453,"image":6585,"meta":6586,"navigation":394,"path":6587,"pinned":452,"published":6588,"seo":6589,"series":7,"seriesOrder":7,"stem":6590,"tags":6591,"updated":6588,"__hash__":6597},"posts\u002Fposts\u002Fjira-cli-intro\u002Findex.md","jira_cli — 把企业 Jira 装进命令行，AI Agent 直接调用",{"type":9,"value":5174,"toc":6563},[5175,5179,5194,5263,5266,5269,5345,5348,5381,5388,5391,5395,5511,5515,5647,5651,5720,5723,5789,5792,5795,5799,5802,5822,5825,5840,5843,5847,5854,5992,6003,6034,6038,6041,6138,6142,6145,6265,6269,6272,6336,6339,6343,6346,6414,6417,6423,6426,6460,6467,6470,6530,6532,6535,6552,6560],[12,5176,5178],{"id":5177},"jira_cli-是什么","jira_cli 是什么",[17,5180,5181,5186,5187,1125,5190,5193],{},[422,5182,5185],{"href":5183,"rel":5184},"https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli",[426],"jira_cli"," 是一个面向企业 Jira（Server \u002F Data Center）的本地命令行工具，Go 语言编写，MIT 协议开源。它同时提供 ",[21,5188,5189],{},"Cobra CLI",[21,5191,5192],{},"MCP stdio server"," 两种入口，既能当日常命令行用，又能直接接入 AI 编程工具。",[191,5195,5197],{"className":357,"code":5196,"language":359,"meta":196,"style":196},"# CLI 日常使用\njira_cli issue search \"project = PROJ ORDER BY updated DESC\"\njira_cli issue get PROJ-123\njira_cli issue comment PROJ-123 \"处理完成，请测试验证\"\n\n# MCP 模式 — 给 AI 装上操作 Jira 的手\njira_cli mcp --user=work\n",[198,5198,5199,5204,5217,5229,5244,5248,5253],{"__ignoreMap":196},[201,5200,5201],{"class":203,"line":204},[201,5202,5203],{"class":207},"# CLI 日常使用\n",[201,5205,5206,5208,5211,5214],{"class":203,"line":211},[201,5207,5185],{"class":291},[201,5209,5210],{"class":238}," issue",[201,5212,5213],{"class":238}," search",[201,5215,5216],{"class":238}," \"project = PROJ ORDER BY updated DESC\"\n",[201,5218,5219,5221,5223,5226],{"class":203,"line":232},[201,5220,5185],{"class":291},[201,5222,5210],{"class":238},[201,5224,5225],{"class":238}," get",[201,5227,5228],{"class":238}," PROJ-123\n",[201,5230,5231,5233,5235,5238,5241],{"class":203,"line":245},[201,5232,5185],{"class":291},[201,5234,5210],{"class":238},[201,5236,5237],{"class":238}," comment",[201,5239,5240],{"class":238}," PROJ-123",[201,5242,5243],{"class":238}," \"处理完成，请测试验证\"\n",[201,5245,5246],{"class":203,"line":256},[201,5247,779],{"emptyLinePlaceholder":394},[201,5249,5250],{"class":203,"line":267},[201,5251,5252],{"class":207},"# MCP 模式 — 给 AI 装上操作 Jira 的手\n",[201,5254,5255,5257,5260],{"class":203,"line":273},[201,5256,5185],{"class":291},[201,5258,5259],{"class":238}," mcp",[201,5261,5262],{"class":221}," --user=work\n",[12,5264,5265],{"id":5265},"为什么需要它",[17,5267,5268],{},"大多数 Jira 命令行工具只解决\"开发者手动操作 Jira\"这一个场景。但 2026 年 AI 编程工具已经普及，你的 AI Agent（Claude Code、Codex、Cursor 等）也需要读写 Jira。痛点就来了：",[35,5270,5271,5281],{},[38,5272,5273],{},[41,5274,5275,5278],{},[44,5276,5277],{},"痛点",[44,5279,5280],{},"jira_cli 怎么解",[51,5282,5283,5291,5299,5311,5322,5333],{},[41,5284,5285,5288],{},[56,5286,5287],{},"企业 Jira 在内网，没有公网 API",[56,5289,5290],{},"本地 stdio MCP server，不经过外网",[41,5292,5293,5296],{},[56,5294,5295],{},"AI 可能误删、误写 Jira 数据",[56,5297,5298],{},"账号级只读策略，AI 只能查不能改",[41,5300,5301,5304],{},[56,5302,5303],{},"团队多人多 Jira 实例",[56,5305,5306,5307,5310],{},"多 Profile 管理，",[198,5308,5309],{},"--user=work"," 切换",[41,5312,5313,5316],{},[56,5314,5315],{},"Windows 终端体验差",[56,5317,5318,5319,5321],{},"原生 ",[198,5320,1861],{}," 构建 + 一键安装脚本",[41,5323,5324,5327],{},[56,5325,5326],{},"AI 生成 JQL 困难",[56,5328,5329,5330],{},"字段元数据发现：",[198,5331,5332],{},"field list --query xxx",[41,5334,5335,5338],{},[56,5336,5337],{},"MCP 直接拿原始 JSON 太冗长",[56,5339,5340,5341,5344],{},"紧凑视图 + ",[198,5342,5343],{},"--view summary"," 减少 token 消耗",[12,5346,5347],{"id":5347},"安装",[191,5349,5351],{"className":357,"code":5350,"language":359,"meta":196,"style":196},"# 从源码安装（要求 Go 1.26.3+）\ngo install github.com\u002Fyourname\u002Fjira_cli@latest\n\n# 或下载预编译二进制\n# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[198,5352,5353,5358,5367,5371,5376],{"__ignoreMap":196},[201,5354,5355],{"class":203,"line":204},[201,5356,5357],{"class":207},"# 从源码安装（要求 Go 1.26.3+）\n",[201,5359,5360,5362,5364],{"class":203,"line":211},[201,5361,759],{"class":291},[201,5363,2290],{"class":238},[201,5365,5366],{"class":238}," github.com\u002Fyourname\u002Fjira_cli@latest\n",[201,5368,5369],{"class":203,"line":232},[201,5370,779],{"emptyLinePlaceholder":394},[201,5372,5373],{"class":203,"line":245},[201,5374,5375],{"class":207},"# 或下载预编译二进制\n",[201,5377,5378],{"class":203,"line":256},[201,5379,5380],{"class":207},"# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[17,5382,5383,5384,5387],{},"Windows 用户下载 zip 包解压后双击 ",[198,5385,5386],{},"install.bat"," 即可全局安装，自动加入 PATH。",[12,5389,5390],{"id":5390},"快速上手",[564,5392,5394],{"id":5393},"_1-配置认证","1. 配置认证",[191,5396,5398],{"className":357,"code":5397,"language":359,"meta":196,"style":196},"# 设置 Jira 地址\njira_cli set base-url https:\u002F\u002Fjira.your-company.com --global\n\n# Token 认证（推荐）\njira_cli set token \u003Cyour-token> --global\n\n# 或用户名密码认证\njira_cli set auth \u003Cusername> \u003Cpassword> --global\n\n# 验证一下\njira_cli user me\n",[198,5399,5400,5405,5421,5425,5430,5452,5456,5461,5492,5496,5501],{"__ignoreMap":196},[201,5401,5402],{"class":203,"line":204},[201,5403,5404],{"class":207},"# 设置 Jira 地址\n",[201,5406,5407,5409,5412,5415,5418],{"class":203,"line":211},[201,5408,5185],{"class":291},[201,5410,5411],{"class":238}," set",[201,5413,5414],{"class":238}," base-url",[201,5416,5417],{"class":238}," https:\u002F\u002Fjira.your-company.com",[201,5419,5420],{"class":221}," --global\n",[201,5422,5423],{"class":203,"line":232},[201,5424,779],{"emptyLinePlaceholder":394},[201,5426,5427],{"class":203,"line":245},[201,5428,5429],{"class":207},"# Token 认证（推荐）\n",[201,5431,5432,5434,5436,5439,5442,5445,5448,5450],{"class":203,"line":256},[201,5433,5185],{"class":291},[201,5435,5411],{"class":238},[201,5437,5438],{"class":238}," token",[201,5440,5441],{"class":214}," \u003C",[201,5443,5444],{"class":238},"your-toke",[201,5446,5447],{"class":228},"n",[201,5449,3343],{"class":214},[201,5451,5420],{"class":221},[201,5453,5454],{"class":203,"line":267},[201,5455,779],{"emptyLinePlaceholder":394},[201,5457,5458],{"class":203,"line":273},[201,5459,5460],{"class":207},"# 或用户名密码认证\n",[201,5462,5463,5465,5467,5470,5472,5475,5478,5480,5482,5485,5488,5490],{"class":203,"line":819},[201,5464,5185],{"class":291},[201,5466,5411],{"class":238},[201,5468,5469],{"class":238}," auth",[201,5471,5441],{"class":214},[201,5473,5474],{"class":238},"usernam",[201,5476,5477],{"class":228},"e",[201,5479,3343],{"class":214},[201,5481,5441],{"class":214},[201,5483,5484],{"class":238},"passwor",[201,5486,5487],{"class":228},"d",[201,5489,3343],{"class":214},[201,5491,5420],{"class":221},[201,5493,5494],{"class":203,"line":825},[201,5495,779],{"emptyLinePlaceholder":394},[201,5497,5498],{"class":203,"line":830},[201,5499,5500],{"class":207},"# 验证一下\n",[201,5502,5503,5505,5508],{"class":203,"line":836},[201,5504,5185],{"class":291},[201,5506,5507],{"class":238}," user",[201,5509,5510],{"class":238}," me\n",[564,5512,5514],{"id":5513},"_2-日常使用","2. 日常使用",[191,5516,5518],{"className":357,"code":5517,"language":359,"meta":196,"style":196},"# 查自己今天的任务\njira_cli issue search \"assignee = currentUser() ORDER BY updated DESC\"\n\n# 看某个任务详情\njira_cli issue get PROJ-123\n\n# 流转状态\njira_cli issue transitions PROJ-123                    # 可以转哪些\njira_cli issue do-transition PROJ-123 21               # 执行流转\n\n# 添加备注\njira_cli issue comment PROJ-123 \"已定位到问题，正在修复\"\n\n# 创建任务\njira_cli issue create PROJ Task --fields @fields.json\n",[198,5519,5520,5525,5536,5540,5545,5555,5559,5564,5578,5595,5599,5604,5617,5621,5626],{"__ignoreMap":196},[201,5521,5522],{"class":203,"line":204},[201,5523,5524],{"class":207},"# 查自己今天的任务\n",[201,5526,5527,5529,5531,5533],{"class":203,"line":211},[201,5528,5185],{"class":291},[201,5530,5210],{"class":238},[201,5532,5213],{"class":238},[201,5534,5535],{"class":238}," \"assignee = currentUser() ORDER BY updated DESC\"\n",[201,5537,5538],{"class":203,"line":232},[201,5539,779],{"emptyLinePlaceholder":394},[201,5541,5542],{"class":203,"line":245},[201,5543,5544],{"class":207},"# 看某个任务详情\n",[201,5546,5547,5549,5551,5553],{"class":203,"line":256},[201,5548,5185],{"class":291},[201,5550,5210],{"class":238},[201,5552,5225],{"class":238},[201,5554,5228],{"class":238},[201,5556,5557],{"class":203,"line":267},[201,5558,779],{"emptyLinePlaceholder":394},[201,5560,5561],{"class":203,"line":273},[201,5562,5563],{"class":207},"# 流转状态\n",[201,5565,5566,5568,5570,5573,5575],{"class":203,"line":819},[201,5567,5185],{"class":291},[201,5569,5210],{"class":238},[201,5571,5572],{"class":238}," transitions",[201,5574,5240],{"class":238},[201,5576,5577],{"class":207},"                    # 可以转哪些\n",[201,5579,5580,5582,5584,5587,5589,5592],{"class":203,"line":825},[201,5581,5185],{"class":291},[201,5583,5210],{"class":238},[201,5585,5586],{"class":238}," do-transition",[201,5588,5240],{"class":238},[201,5590,5591],{"class":221}," 21",[201,5593,5594],{"class":207},"               # 执行流转\n",[201,5596,5597],{"class":203,"line":830},[201,5598,779],{"emptyLinePlaceholder":394},[201,5600,5601],{"class":203,"line":836},[201,5602,5603],{"class":207},"# 添加备注\n",[201,5605,5606,5608,5610,5612,5614],{"class":203,"line":853},[201,5607,5185],{"class":291},[201,5609,5210],{"class":238},[201,5611,5237],{"class":238},[201,5613,5240],{"class":238},[201,5615,5616],{"class":238}," \"已定位到问题，正在修复\"\n",[201,5618,5619],{"class":203,"line":858},[201,5620,779],{"emptyLinePlaceholder":394},[201,5622,5623],{"class":203,"line":864},[201,5624,5625],{"class":207},"# 创建任务\n",[201,5627,5628,5630,5632,5635,5638,5641,5644],{"class":203,"line":903},[201,5629,5185],{"class":291},[201,5631,5210],{"class":238},[201,5633,5634],{"class":238}," create",[201,5636,5637],{"class":238}," PROJ",[201,5639,5640],{"class":238}," Task",[201,5642,5643],{"class":221}," --fields",[201,5645,5646],{"class":238}," @fields.json\n",[564,5648,5650],{"id":5649},"_3-配给-ai-使用","3. 配给 AI 使用",[191,5652,5654],{"className":357,"code":5653,"language":359,"meta":196,"style":196},"# 先配置一个只读账号给 AI 用\njira_cli set base-url https:\u002F\u002Fjira.your-company.com --name ai --global\njira_cli set token \u003Ctoken> --name ai --global\njira_cli set readonly on --name ai --global\n",[198,5655,5656,5661,5679,5702],{"__ignoreMap":196},[201,5657,5658],{"class":203,"line":204},[201,5659,5660],{"class":207},"# 先配置一个只读账号给 AI 用\n",[201,5662,5663,5665,5667,5669,5671,5674,5677],{"class":203,"line":211},[201,5664,5185],{"class":291},[201,5666,5411],{"class":238},[201,5668,5414],{"class":238},[201,5670,5417],{"class":238},[201,5672,5673],{"class":221}," --name",[201,5675,5676],{"class":238}," ai",[201,5678,5420],{"class":221},[201,5680,5681,5683,5685,5687,5689,5692,5694,5696,5698,5700],{"class":203,"line":232},[201,5682,5185],{"class":291},[201,5684,5411],{"class":238},[201,5686,5438],{"class":238},[201,5688,5441],{"class":214},[201,5690,5691],{"class":238},"toke",[201,5693,5447],{"class":228},[201,5695,3343],{"class":214},[201,5697,5673],{"class":221},[201,5699,5676],{"class":238},[201,5701,5420],{"class":221},[201,5703,5704,5706,5708,5711,5714,5716,5718],{"class":203,"line":245},[201,5705,5185],{"class":291},[201,5707,5411],{"class":238},[201,5709,5710],{"class":238}," readonly",[201,5712,5713],{"class":238}," on",[201,5715,5673],{"class":221},[201,5717,5676],{"class":238},[201,5719,5420],{"class":221},[17,5721,5722],{},"然后在 AI 工具的 MCP 配置里加上：",[191,5724,5726],{"className":719,"code":5725,"language":721,"meta":196,"style":196},"{\n  \"mcpServers\": {\n    \"jira\": {\n      \"command\": \"jira_cli\",\n      \"args\": [\"mcp\", \"--user=ai\"]\n    }\n  }\n}\n",[198,5727,5728,5733,5740,5747,5759,5776,5780,5785],{"__ignoreMap":196},[201,5729,5730],{"class":203,"line":204},[201,5731,5732],{"class":228},"{\n",[201,5734,5735,5738],{"class":203,"line":211},[201,5736,5737],{"class":221},"  \"mcpServers\"",[201,5739,736],{"class":228},[201,5741,5742,5745],{"class":203,"line":232},[201,5743,5744],{"class":221},"    \"jira\"",[201,5746,736],{"class":228},[201,5748,5749,5752,5754,5757],{"class":203,"line":245},[201,5750,5751],{"class":221},"      \"command\"",[201,5753,301],{"class":228},[201,5755,5756],{"class":238},"\"jira_cli\"",[201,5758,242],{"class":228},[201,5760,5761,5764,5766,5769,5771,5774],{"class":203,"line":256},[201,5762,5763],{"class":221},"      \"args\"",[201,5765,332],{"class":228},[201,5767,5768],{"class":238},"\"mcp\"",[201,5770,338],{"class":228},[201,5772,5773],{"class":238},"\"--user=ai\"",[201,5775,344],{"class":228},[201,5777,5778],{"class":203,"line":267},[201,5779,1081],{"class":228},[201,5781,5782],{"class":203,"line":273},[201,5783,5784],{"class":228},"  }\n",[201,5786,5787],{"class":203,"line":819},[201,5788,276],{"class":228},[17,5790,5791],{},"这样 AI 就能查 Jira 但不能修改，既有了上下文，又不用担心误操作。",[12,5793,5794],{"id":5794},"亮点",[564,5796,5798],{"id":5797},"_1-账号级只读策略","1. 账号级只读策略",[17,5800,5801],{},"这是 jira_cli 最实用的设计。给 AI Agent 配置只读账号：",[191,5803,5805],{"className":357,"code":5804,"language":359,"meta":196,"style":196},"jira_cli set readonly on --name ai\n",[198,5806,5807],{"__ignoreMap":196},[201,5808,5809,5811,5813,5815,5817,5819],{"class":203,"line":204},[201,5810,5185],{"class":291},[201,5812,5411],{"class":238},[201,5814,5710],{"class":238},[201,5816,5713],{"class":238},[201,5818,5673],{"class":221},[201,5820,5821],{"class":238}," ai\n",[17,5823,5824],{},"只读模式下：",[121,5826,5827,5830,5833],{},[124,5828,5829],{},"CLI 写命令被隐藏和拒绝",[124,5831,5832],{},"MCP 的写入工具不会注册",[124,5834,5835,5836,5839],{},"环境变量 ",[198,5837,5838],{},"JIRA_READONLY=true"," 也可以临时覆盖",[17,5841,5842],{},"AI 只能查 JQL、看任务、查字段、读评论，绝对不会误改数据。需要创建\u002F更新时，另配一个有写入权限的账号由开发者自己操作。",[564,5844,5846],{"id":5845},"_2-多-profile-切换","2. 多 Profile 切换",[17,5848,5849,5850,5853],{},"一个配置文件里管理多个 Jira 账号，",[198,5851,5852],{},"--user"," 切换：",[191,5855,5857],{"className":357,"code":5856,"language":359,"meta":196,"style":196},"# 工作账号\njira_cli set base-url https:\u002F\u002Fjira.company.com --name work --global\njira_cli set token \u003Cwork-token> --name work --global\n\n# 个人账号\njira_cli set base-url https:\u002F\u002Fjira.personal.com --name personal --global\njira_cli set token \u003Cpersonal-token> --name personal --global\n\n# 使用时切换\njira_cli --user=work issue search \"project = PROJ\"\njira_cli --user=personal issue search \"project = SIDE\"\n",[198,5858,5859,5864,5882,5905,5909,5914,5932,5955,5959,5964,5978],{"__ignoreMap":196},[201,5860,5861],{"class":203,"line":204},[201,5862,5863],{"class":207},"# 工作账号\n",[201,5865,5866,5868,5870,5872,5875,5877,5880],{"class":203,"line":211},[201,5867,5185],{"class":291},[201,5869,5411],{"class":238},[201,5871,5414],{"class":238},[201,5873,5874],{"class":238}," https:\u002F\u002Fjira.company.com",[201,5876,5673],{"class":221},[201,5878,5879],{"class":238}," work",[201,5881,5420],{"class":221},[201,5883,5884,5886,5888,5890,5892,5895,5897,5899,5901,5903],{"class":203,"line":232},[201,5885,5185],{"class":291},[201,5887,5411],{"class":238},[201,5889,5438],{"class":238},[201,5891,5441],{"class":214},[201,5893,5894],{"class":238},"work-toke",[201,5896,5447],{"class":228},[201,5898,3343],{"class":214},[201,5900,5673],{"class":221},[201,5902,5879],{"class":238},[201,5904,5420],{"class":221},[201,5906,5907],{"class":203,"line":245},[201,5908,779],{"emptyLinePlaceholder":394},[201,5910,5911],{"class":203,"line":256},[201,5912,5913],{"class":207},"# 个人账号\n",[201,5915,5916,5918,5920,5922,5925,5927,5930],{"class":203,"line":267},[201,5917,5185],{"class":291},[201,5919,5411],{"class":238},[201,5921,5414],{"class":238},[201,5923,5924],{"class":238}," https:\u002F\u002Fjira.personal.com",[201,5926,5673],{"class":221},[201,5928,5929],{"class":238}," personal",[201,5931,5420],{"class":221},[201,5933,5934,5936,5938,5940,5942,5945,5947,5949,5951,5953],{"class":203,"line":273},[201,5935,5185],{"class":291},[201,5937,5411],{"class":238},[201,5939,5438],{"class":238},[201,5941,5441],{"class":214},[201,5943,5944],{"class":238},"personal-toke",[201,5946,5447],{"class":228},[201,5948,3343],{"class":214},[201,5950,5673],{"class":221},[201,5952,5929],{"class":238},[201,5954,5420],{"class":221},[201,5956,5957],{"class":203,"line":819},[201,5958,779],{"emptyLinePlaceholder":394},[201,5960,5961],{"class":203,"line":825},[201,5962,5963],{"class":207},"# 使用时切换\n",[201,5965,5966,5968,5971,5973,5975],{"class":203,"line":830},[201,5967,5185],{"class":291},[201,5969,5970],{"class":221}," --user=work",[201,5972,5210],{"class":238},[201,5974,5213],{"class":238},[201,5976,5977],{"class":238}," \"project = PROJ\"\n",[201,5979,5980,5982,5985,5987,5989],{"class":203,"line":836},[201,5981,5185],{"class":291},[201,5983,5984],{"class":221}," --user=personal",[201,5986,5210],{"class":238},[201,5988,5213],{"class":238},[201,5990,5991],{"class":238}," \"project = SIDE\"\n",[17,5993,5994,5995,5998,5999,6002],{},"同时支持",[21,5996,5997],{},"项目级配置","（",[198,6000,6001],{},"--local","）和工作目录绑定：",[191,6004,6006],{"className":357,"code":6005,"language":359,"meta":196,"style":196},"# 在项目目录下，配置只对当前项目生效\ncd ~\u002Fprojects\u002Fbackend\njira_cli set base-url https:\u002F\u002Fjira-backend.company.com --local\n",[198,6007,6008,6013,6020],{"__ignoreMap":196},[201,6009,6010],{"class":203,"line":204},[201,6011,6012],{"class":207},"# 在项目目录下，配置只对当前项目生效\n",[201,6014,6015,6017],{"class":203,"line":211},[201,6016,1552],{"class":221},[201,6018,6019],{"class":238}," ~\u002Fprojects\u002Fbackend\n",[201,6021,6022,6024,6026,6028,6031],{"class":203,"line":232},[201,6023,5185],{"class":291},[201,6025,5411],{"class":238},[201,6027,5414],{"class":238},[201,6029,6030],{"class":238}," https:\u002F\u002Fjira-backend.company.com",[201,6032,6033],{"class":221}," --local\n",[564,6035,6037],{"id":6036},"_3-对-ai-友好的输出","3. 对 AI 友好的输出",[17,6039,6040],{},"jira_cli 在输出设计上充分考虑了 AI Agent 的 token 消耗：",[191,6042,6044],{"className":357,"code":6043,"language":359,"meta":196,"style":196},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\njira_cli issue get PROJ-123 --view summary\n\n# 表格输出 — 一眼看到关键信息\njira_cli issue search \"project = PROJ\" --output table\n\n# JSON 输出 — 程序解析，不转义 Unicode\njira_cli --output json issue get PROJ-123\n\n# 自定义搜索列 — 只要需要的字段\njira_cli set search-columns key,summary,fixVersions\n",[198,6045,6046,6051,6067,6071,6076,6093,6097,6102,6117,6121,6126],{"__ignoreMap":196},[201,6047,6048],{"class":203,"line":204},[201,6049,6050],{"class":207},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\n",[201,6052,6053,6055,6057,6059,6061,6064],{"class":203,"line":211},[201,6054,5185],{"class":291},[201,6056,5210],{"class":238},[201,6058,5225],{"class":238},[201,6060,5240],{"class":238},[201,6062,6063],{"class":221}," --view",[201,6065,6066],{"class":238}," summary\n",[201,6068,6069],{"class":203,"line":232},[201,6070,779],{"emptyLinePlaceholder":394},[201,6072,6073],{"class":203,"line":245},[201,6074,6075],{"class":207},"# 表格输出 — 一眼看到关键信息\n",[201,6077,6078,6080,6082,6084,6087,6090],{"class":203,"line":256},[201,6079,5185],{"class":291},[201,6081,5210],{"class":238},[201,6083,5213],{"class":238},[201,6085,6086],{"class":238}," \"project = PROJ\"",[201,6088,6089],{"class":221}," --output",[201,6091,6092],{"class":238}," table\n",[201,6094,6095],{"class":203,"line":267},[201,6096,779],{"emptyLinePlaceholder":394},[201,6098,6099],{"class":203,"line":273},[201,6100,6101],{"class":207},"# JSON 输出 — 程序解析，不转义 Unicode\n",[201,6103,6104,6106,6108,6111,6113,6115],{"class":203,"line":819},[201,6105,5185],{"class":291},[201,6107,6089],{"class":221},[201,6109,6110],{"class":238}," json",[201,6112,5210],{"class":238},[201,6114,5225],{"class":238},[201,6116,5228],{"class":238},[201,6118,6119],{"class":203,"line":825},[201,6120,779],{"emptyLinePlaceholder":394},[201,6122,6123],{"class":203,"line":830},[201,6124,6125],{"class":207},"# 自定义搜索列 — 只要需要的字段\n",[201,6127,6128,6130,6132,6135],{"class":203,"line":836},[201,6129,5185],{"class":291},[201,6131,5411],{"class":238},[201,6133,6134],{"class":238}," search-columns",[201,6136,6137],{"class":238}," key,summary,fixVersions\n",[564,6139,6141],{"id":6140},"_4-字段元数据发现","4. 字段元数据发现",[17,6143,6144],{},"Jira 的 customfield 让人头疼，jira_cli 提供了完整的字段发现能力：",[191,6146,6148],{"className":357,"code":6147,"language":359,"meta":196,"style":196},"# 列出所有字段及其类型\njira_cli field list\n\n# 搜索包含特定关键词的字段\njira_cli field list --query \"版本\"\n\n# 只看自定义字段\njira_cli field list --custom-only\n\n# 创建任务前，先看看需要哪些字段\njira_cli issue create-meta PROJ --issue-type Task\n\n# 分页查看可选值\njira_cli issue create-meta-values PROJ Task priority --query High\n",[198,6149,6150,6155,6165,6169,6174,6189,6193,6198,6209,6213,6218,6235,6239,6244],{"__ignoreMap":196},[201,6151,6152],{"class":203,"line":204},[201,6153,6154],{"class":207},"# 列出所有字段及其类型\n",[201,6156,6157,6159,6162],{"class":203,"line":211},[201,6158,5185],{"class":291},[201,6160,6161],{"class":238}," field",[201,6163,6164],{"class":238}," list\n",[201,6166,6167],{"class":203,"line":232},[201,6168,779],{"emptyLinePlaceholder":394},[201,6170,6171],{"class":203,"line":245},[201,6172,6173],{"class":207},"# 搜索包含特定关键词的字段\n",[201,6175,6176,6178,6180,6183,6186],{"class":203,"line":256},[201,6177,5185],{"class":291},[201,6179,6161],{"class":238},[201,6181,6182],{"class":238}," list",[201,6184,6185],{"class":221}," --query",[201,6187,6188],{"class":238}," \"版本\"\n",[201,6190,6191],{"class":203,"line":267},[201,6192,779],{"emptyLinePlaceholder":394},[201,6194,6195],{"class":203,"line":273},[201,6196,6197],{"class":207},"# 只看自定义字段\n",[201,6199,6200,6202,6204,6206],{"class":203,"line":819},[201,6201,5185],{"class":291},[201,6203,6161],{"class":238},[201,6205,6182],{"class":238},[201,6207,6208],{"class":221}," --custom-only\n",[201,6210,6211],{"class":203,"line":825},[201,6212,779],{"emptyLinePlaceholder":394},[201,6214,6215],{"class":203,"line":830},[201,6216,6217],{"class":207},"# 创建任务前，先看看需要哪些字段\n",[201,6219,6220,6222,6224,6227,6229,6232],{"class":203,"line":836},[201,6221,5185],{"class":291},[201,6223,5210],{"class":238},[201,6225,6226],{"class":238}," create-meta",[201,6228,5637],{"class":238},[201,6230,6231],{"class":221}," --issue-type",[201,6233,6234],{"class":238}," Task\n",[201,6236,6237],{"class":203,"line":853},[201,6238,779],{"emptyLinePlaceholder":394},[201,6240,6241],{"class":203,"line":858},[201,6242,6243],{"class":207},"# 分页查看可选值\n",[201,6245,6246,6248,6250,6253,6255,6257,6260,6262],{"class":203,"line":864},[201,6247,5185],{"class":291},[201,6249,5210],{"class":238},[201,6251,6252],{"class":238}," create-meta-values",[201,6254,5637],{"class":238},[201,6256,5640],{"class":238},[201,6258,6259],{"class":238}," priority",[201,6261,6185],{"class":221},[201,6263,6264],{"class":238}," High\n",[564,6266,6268],{"id":6267},"_5-mcp-写入控制","5. MCP 写入控制",[17,6270,6271],{},"除了全局只读，MCP 还有更细粒度的控制：",[35,6273,6274,6284],{},[38,6275,6276],{},[41,6277,6278,6281],{},[44,6279,6280],{},"环境变量",[44,6282,6283],{},"作用",[51,6285,6286,6296,6306,6316,6326],{},[41,6287,6288,6293],{},[56,6289,6290],{},[198,6291,6292],{},"JIRA_CREATE_OPEN=false",[56,6294,6295],{},"禁用创建\u002F更新工具",[41,6297,6298,6303],{},[56,6299,6300],{},[198,6301,6302],{},"JIRA_DISABLE_DISCLAIMER=true",[56,6304,6305],{},"去掉 AI 自动添加的备注声明",[41,6307,6308,6313],{},[56,6309,6310],{},[198,6311,6312],{},"JIRA_CREATE_SUMMARY_MAX",[56,6314,6315],{},"限制摘要长度",[41,6317,6318,6323],{},[56,6319,6320],{},[198,6321,6322],{},"JIRA_CREATE_DESCRIPTION_MAX",[56,6324,6325],{},"限制描述长度",[41,6327,6328,6333],{},[56,6329,6330],{},[198,6331,6332],{},"JIRA_CREATE_FIELDS_MAX_BYTES",[56,6334,6335],{},"限制创建字段的 JSON 大小",[17,6337,6338],{},"这些限制确保 AI 即使有写入权限也不会\"失控\"——例如防止 AI 往描述里塞几万字的废话。",[564,6340,6342],{"id":6341},"_6-中英双语帮助","6. 中英双语帮助",[17,6344,6345],{},"帮助信息默认中文，也支持一键切英文：",[191,6347,6349],{"className":357,"code":6348,"language":359,"meta":196,"style":196},"# 保存英文偏好\njira_cli set help-language en\n\n# 切回中文\njira_cli set help-language zh\n\n# 临时覆盖\njira_cli --lang en issue search -h\n",[198,6350,6351,6356,6368,6372,6377,6388,6392,6397],{"__ignoreMap":196},[201,6352,6353],{"class":203,"line":204},[201,6354,6355],{"class":207},"# 保存英文偏好\n",[201,6357,6358,6360,6362,6365],{"class":203,"line":211},[201,6359,5185],{"class":291},[201,6361,5411],{"class":238},[201,6363,6364],{"class":238}," help-language",[201,6366,6367],{"class":238}," en\n",[201,6369,6370],{"class":203,"line":232},[201,6371,779],{"emptyLinePlaceholder":394},[201,6373,6374],{"class":203,"line":245},[201,6375,6376],{"class":207},"# 切回中文\n",[201,6378,6379,6381,6383,6385],{"class":203,"line":256},[201,6380,5185],{"class":291},[201,6382,5411],{"class":238},[201,6384,6364],{"class":238},[201,6386,6387],{"class":238}," zh\n",[201,6389,6390],{"class":203,"line":267},[201,6391,779],{"emptyLinePlaceholder":394},[201,6393,6394],{"class":203,"line":273},[201,6395,6396],{"class":207},"# 临时覆盖\n",[201,6398,6399,6401,6404,6407,6409,6411],{"class":203,"line":819},[201,6400,5185],{"class":291},[201,6402,6403],{"class":221}," --lang",[201,6405,6406],{"class":238}," en",[201,6408,5210],{"class":238},[201,6410,5213],{"class":238},[201,6412,6413],{"class":221}," -h\n",[12,6415,6416],{"id":6416},"架构一览",[191,6418,6421],{"className":6419,"code":6420,"language":3483},[3481],"main.go\n  cmd\u002F                         CLI 命令、MCP 启动、访问策略\n  internal\u002Fconfig\u002F             运行时配置、Profile 持久化\n  internal\u002Ftools\u002Fjira_tools\u002F   MCP 输入\u002F输出适配层\n  pkg\u002Fjira\u002F                    可复用的 Jira REST 客户端\n",[198,6422,6420],{"__ignoreMap":196},[17,6424,6425],{},"设计原则清晰：",[121,6427,6428,6436,6444,6452],{},[124,6429,6430,6435],{},[21,6431,6432],{},[198,6433,6434],{},"pkg\u002Fjira"," 是纯 Jira 客户端，HTTP、认证、上下文处理都在这里",[124,6437,6438,6443],{},[21,6439,6440],{},[198,6441,6442],{},"cmd"," 只管 Cobra 解析和呈现",[124,6445,6446,6451],{},[21,6447,6448],{},[198,6449,6450],{},"internal\u002Ftools\u002Fjira_tools"," 只管 MCP schema 验证和结果适配",[124,6453,6454,6459],{},[21,6455,6456],{},[198,6457,6458],{},"internal\u002Fconfig"," 拥有 Profile 的全部逻辑，不分散到 cmd",[17,6461,6462,6463,6466],{},"每一层职责单一，新增 Jira 能力时按 ",[198,6464,6465],{},"endpoints.go → Client 方法 → CLI 命令 → MCP tool → 策略注册"," 的路径走就行。",[12,6468,6469],{"id":6469},"和已有方案的对比",[35,6471,6472,6484],{},[38,6473,6474],{},[41,6475,6476,6478,6481],{},[44,6477,2176],{},[44,6479,6480],{},"适合场景",[44,6482,6483],{},"jira_cli 的区别",[51,6485,6486,6497,6508,6519],{},[41,6487,6488,6491,6494],{},[56,6489,6490],{},"Atlassian Rovo MCP Server",[56,6492,6493],{},"Jira Cloud、官方 OAuth",[56,6495,6496],{},"jira_cli 是本地 stdio，适合内网 Server\u002FDC",[41,6498,6499,6502,6505],{},[56,6500,6501],{},"go-jira \u002F jira-cli 社区版",[56,6503,6504],{},"纯命令行管理",[56,6506,6507],{},"jira_cli 同时提供 MCP server，面向 AI 工作流",[41,6509,6510,6513,6516],{},[56,6511,6512],{},"直接调 REST API",[56,6514,6515],{},"自定义集成",[56,6517,6518],{},"jira_cli 封装了认证、只读策略、字段元数据",[41,6520,6521,6524,6527],{},[56,6522,6523],{},"Jira 网页",[56,6525,6526],{},"日常使用",[56,6528,6529],{},"jira_cli 把 Jira 变成终端里的一行命令",[12,6531,2054],{"id":2054},[17,6533,6534],{},"如果你的团队满足以下任一条件，jira_cli 值得一试：",[121,6536,6537,6540,6543,6546,6549],{},[124,6538,6539],{},"企业 Jira 部署在内网，想接入 AI 编程工具",[124,6541,6542],{},"想给 AI Agent 只读权限来查 Jira 上下文",[124,6544,6545],{},"需要命令行快速查看\u002F操作 Jira，不想切浏览器",[124,6547,6548],{},"需要管理多个 Jira 实例和多套账号",[124,6550,6551],{},"Windows 用户想要无摩擦的 CLI 体验",[17,6553,6554,6555,6559],{},"GitHub：",[422,6556,6558],{"href":5183,"rel":6557},[426],"github.com\u002Fyourname\u002Fjira_cli"," | MIT 协议 | Go 1.26.3+",[438,6561,6562],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}",{"title":196,"searchDepth":211,"depth":232,"links":6564},[6565,6566,6567,6568,6573,6581,6582,6583],{"id":5177,"depth":211,"text":5178},{"id":5265,"depth":211,"text":5265},{"id":5347,"depth":211,"text":5347},{"id":5390,"depth":211,"text":5390,"children":6569},[6570,6571,6572],{"id":5393,"depth":232,"text":5394},{"id":5513,"depth":232,"text":5514},{"id":5649,"depth":232,"text":5650},{"id":5794,"depth":211,"text":5794,"children":6574},[6575,6576,6577,6578,6579,6580],{"id":5797,"depth":232,"text":5798},{"id":5845,"depth":232,"text":5846},{"id":6036,"depth":232,"text":6037},{"id":6140,"depth":232,"text":6141},{"id":6267,"depth":232,"text":6268},{"id":6341,"depth":232,"text":6342},{"id":6416,"depth":211,"text":6416},{"id":6469,"depth":211,"text":6469},{"id":2054,"depth":211,"text":2054},"介绍 jira_cli，一个开源的企业 Jira CLI 命令行工具和MCP，支持 Jira Server \u002F Data Center，自带 MCP stdio server，让 Claude Code、Codex 等 AI 编程工具直接读写 Jira 任务。","\u002Fposts\u002Fjira-cli-intro\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fjira-cli-intro","2026-07-17",{"title":5172,"description":6584},"posts\u002Fjira-cli-intro\u002Findex",[6592,6593,6594,2126,6595,6596],"Jira","CLI","MCP","AI Agent","效率工具","vNH6sF2mMFmSjbHoEI2OqmYCgpFmaqyKt-MUfcQqXrU",{"id":6599,"title":6600,"author":7,"body":6601,"description":7545,"difficulty":2117,"draft":452,"extension":453,"image":7546,"meta":7547,"navigation":394,"path":479,"pinned":452,"published":7548,"seo":7549,"series":2123,"seriesOrder":211,"stem":7550,"tags":7551,"updated":7555,"__hash__":7556},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",{"type":9,"value":6602,"toc":7528},[6603,6605,6612,6615,6617,6626,6628,6632,6958,6962,7070,7074,7207,7209,7213,7222,7237,7246,7264,7278,7284,7292,7392,7398,7402,7408,7417,7419,7455,7458,7463,7502,7504,7510,7522,7525],[12,6604,473],{"id":473},[17,6606,6607,6608,6611],{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[198,6609,6610],{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[17,6613,6614],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[12,6616,548],{"id":548},[17,6618,6619,6620,6622,6623,6625],{},"利用 Go 1.16 引入的 ",[198,6621,554],{}," 指令，在编译时将前端 ",[198,6624,558],{}," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[12,6627,562],{"id":562},[564,6629,6631],{"id":6630},"_1-创建嵌入模块","1. 创建嵌入模块",[191,6633,6635],{"className":757,"code":6634,"language":759,"meta":196,"style":196},"\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",[198,6636,6637,6642,6648,6652,6658,6666,6675,6684,6692,6701,6709,6713,6722,6726,6730,6735,6748,6752,6770,6790,6794,6815,6832,6844,6854,6858,6862,6876,6888,6894,6914,6919,6924,6948,6953],{"__ignoreMap":196},[201,6638,6639],{"class":203,"line":204},[201,6640,6641],{"class":207},"\u002F\u002F service\u002Fweb\u002Fembed.go\n",[201,6643,6644,6646],{"class":203,"line":211},[201,6645,771],{"class":214},[201,6647,774],{"class":291},[201,6649,6650],{"class":203,"line":232},[201,6651,779],{"emptyLinePlaceholder":394},[201,6653,6654,6656],{"class":203,"line":245},[201,6655,784],{"class":214},[201,6657,787],{"class":228},[201,6659,6660,6662,6664],{"class":203,"line":256},[201,6661,792],{"class":238},[201,6663,795],{"class":291},[201,6665,798],{"class":238},[201,6667,6668,6670,6673],{"class":203,"line":267},[201,6669,792],{"class":238},[201,6671,6672],{"class":291},"io\u002Ffs",[201,6674,798],{"class":238},[201,6676,6677,6679,6682],{"class":203,"line":273},[201,6678,792],{"class":238},[201,6680,6681],{"class":291},"mime",[201,6683,798],{"class":238},[201,6685,6686,6688,6690],{"class":203,"line":819},[201,6687,792],{"class":238},[201,6689,805],{"class":291},[201,6691,798],{"class":238},[201,6693,6694,6696,6699],{"class":203,"line":825},[201,6695,792],{"class":238},[201,6697,6698],{"class":291},"path\u002Ffilepath",[201,6700,798],{"class":238},[201,6702,6703,6705,6707],{"class":203,"line":830},[201,6704,792],{"class":238},[201,6706,814],{"class":291},[201,6708,798],{"class":238},[201,6710,6711],{"class":203,"line":836},[201,6712,779],{"emptyLinePlaceholder":394},[201,6714,6715,6717,6720],{"class":203,"line":853},[201,6716,792],{"class":238},[201,6718,6719],{"class":291},"github.com\u002Fgin-gonic\u002Fgin",[201,6721,798],{"class":238},[201,6723,6724],{"class":203,"line":858},[201,6725,822],{"class":228},[201,6727,6728],{"class":203,"line":864},[201,6729,779],{"emptyLinePlaceholder":394},[201,6731,6732],{"class":203,"line":903},[201,6733,6734],{"class":207},"\u002F\u002Fgo:embed dist\n",[201,6736,6737,6739,6742,6744,6746],{"class":203,"line":908},[201,6738,839],{"class":214},[201,6740,6741],{"class":228}," embedFS ",[201,6743,795],{"class":291},[201,6745,847],{"class":228},[201,6747,850],{"class":291},[201,6749,6750],{"class":203,"line":922},[201,6751,779],{"emptyLinePlaceholder":394},[201,6753,6754,6756,6759,6761,6763,6765,6768],{"class":203,"line":931},[201,6755,959],{"class":214},[201,6757,6758],{"class":291}," SPAHandler",[201,6760,4073],{"class":228},[201,6762,1199],{"class":291},[201,6764,847],{"class":228},[201,6766,6767],{"class":291},"HandlerFunc",[201,6769,229],{"class":228},[201,6771,6772,6775,6777,6780,6783,6786,6788],{"class":203,"line":946},[201,6773,6774],{"class":228},"    sub, _ ",[201,6776,1214],{"class":214},[201,6778,6779],{"class":228}," fs.",[201,6781,6782],{"class":291},"Sub",[201,6784,6785],{"class":228},"(embedFS, ",[201,6787,891],{"class":238},[201,6789,822],{"class":228},[201,6791,6792],{"class":203,"line":951},[201,6793,779],{"emptyLinePlaceholder":394},[201,6795,6796,6798,6801,6803,6805,6807,6809,6811,6813],{"class":203,"line":956},[201,6797,1087],{"class":214},[201,6799,6800],{"class":214}," func",[201,6802,979],{"class":228},[201,6804,1265],{"class":965},[201,6806,1177],{"class":214},[201,6808,1199],{"class":291},[201,6810,847],{"class":228},[201,6812,1274],{"class":291},[201,6814,1003],{"class":228},[201,6816,6817,6819,6821,6823,6825,6828,6830],{"class":203,"line":1006},[201,6818,1326],{"class":228},[201,6820,1214],{"class":214},[201,6822,1064],{"class":228},[201,6824,1067],{"class":291},[201,6826,6827],{"class":228},"(c.Request.URL.Path, ",[201,6829,1073],{"class":238},[201,6831,822],{"class":228},[201,6833,6834,6836,6838,6840,6842],{"class":203,"line":1023},[201,6835,1346],{"class":214},[201,6837,1349],{"class":228},[201,6839,1015],{"class":214},[201,6841,2810],{"class":238},[201,6843,229],{"class":228},[201,6845,6846,6849,6851],{"class":203,"line":1035},[201,6847,6848],{"class":228},"            p ",[201,6850,1029],{"class":214},[201,6852,6853],{"class":238}," \"index.html\"\n",[201,6855,6856],{"class":203,"line":1046},[201,6857,1440],{"class":228},[201,6859,6860],{"class":203,"line":1078},[201,6861,779],{"emptyLinePlaceholder":394},[201,6863,6864,6867,6869,6871,6873],{"class":203,"line":1084},[201,6865,6866],{"class":228},"        data, err ",[201,6868,1214],{"class":214},[201,6870,6779],{"class":228},[201,6872,4090],{"class":291},[201,6874,6875],{"class":228},"(sub, p)\n",[201,6877,6878,6880,6882,6884,6886],{"class":203,"line":1104},[201,6879,1346],{"class":214},[201,6881,3790],{"class":228},[201,6883,3793],{"class":214},[201,6885,3796],{"class":221},[201,6887,229],{"class":228},[201,6889,6891],{"class":203,"line":6890},29,[201,6892,6893],{"class":207},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[201,6895,6897,6900,6902,6904,6906,6909,6912],{"class":203,"line":6896},30,[201,6898,6899],{"class":228},"            data, _ ",[201,6901,1029],{"class":214},[201,6903,6779],{"class":228},[201,6905,4090],{"class":291},[201,6907,6908],{"class":228},"(sub, ",[201,6910,6911],{"class":238},"\"index.html\"",[201,6913,822],{"class":228},[201,6915,6917],{"class":203,"line":6916},31,[201,6918,1440],{"class":228},[201,6920,6922],{"class":203,"line":6921},32,[201,6923,779],{"emptyLinePlaceholder":394},[201,6925,6927,6930,6933,6936,6939,6942,6945],{"class":203,"line":6926},33,[201,6928,6929],{"class":228},"        c.",[201,6931,6932],{"class":291},"Data",[201,6934,6935],{"class":228},"(http.StatusOK, mime.",[201,6937,6938],{"class":291},"TypeByExtension",[201,6940,6941],{"class":228},"(filepath.",[201,6943,6944],{"class":291},"Ext",[201,6946,6947],{"class":228},"(p)), data)\n",[201,6949,6951],{"class":203,"line":6950},34,[201,6952,1081],{"class":228},[201,6954,6956],{"class":203,"line":6955},35,[201,6957,276],{"class":228},[564,6959,6961],{"id":6960},"_2-改造路由","2. 改造路由",[191,6963,6965],{"className":757,"code":6964,"language":759,"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,6966,6967,6972,6983,6987,6992,7007,7021,7034,7047,7060],{"__ignoreMap":196},[201,6968,6969],{"class":203,"line":204},[201,6970,6971],{"class":207},"\u002F\u002F router.go\n",[201,6973,6974,6976,6978,6981],{"class":203,"line":211},[201,6975,784],{"class":214},[201,6977,1561],{"class":238},[201,6979,6980],{"class":291},"myproject\u002Fweb",[201,6982,798],{"class":238},[201,6984,6985],{"class":203,"line":232},[201,6986,779],{"emptyLinePlaceholder":394},[201,6988,6989],{"class":203,"line":245},[201,6990,6991],{"class":207},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[201,6993,6994,6997,6999,7002,7005],{"class":203,"line":256},[201,6995,6996],{"class":228},"webHandler ",[201,6998,1214],{"class":214},[201,7000,7001],{"class":228}," web.",[201,7003,7004],{"class":291},"SPAHandler",[201,7006,1223],{"class":228},[201,7008,7009,7012,7014,7016,7018],{"class":203,"line":267},[201,7010,7011],{"class":228},"router.",[201,7013,1251],{"class":291},[201,7015,979],{"class":228},[201,7017,1073],{"class":238},[201,7019,7020],{"class":228},", webHandler)\n",[201,7022,7023,7025,7027,7029,7032],{"class":203,"line":273},[201,7024,7011],{"class":228},[201,7026,1251],{"class":291},[201,7028,979],{"class":228},[201,7030,7031],{"class":238},"\"\u002Fassets\u002F*filepath\"",[201,7033,7020],{"class":228},[201,7035,7036,7038,7040,7042,7045],{"class":203,"line":819},[201,7037,7011],{"class":228},[201,7039,1251],{"class":291},[201,7041,979],{"class":228},[201,7043,7044],{"class":238},"\"\u002Ffavicon.ico\"",[201,7046,7020],{"class":228},[201,7048,7049,7051,7053,7055,7058],{"class":203,"line":825},[201,7050,7011],{"class":228},[201,7052,1251],{"class":291},[201,7054,979],{"class":228},[201,7056,7057],{"class":238},"\"\u002Ffavicon.svg\"",[201,7059,7020],{"class":228},[201,7061,7062,7064,7067],{"class":203,"line":830},[201,7063,7011],{"class":228},[201,7065,7066],{"class":291},"NoRoute",[201,7068,7069],{"class":228},"(webHandler)\n",[564,7071,7073],{"id":7072},"_3-一键构建脚本","3. 一键构建脚本",[191,7075,7077],{"className":357,"code":7076,"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,7078,7079,7084,7089,7093,7100,7104,7109,7119,7129,7133,7138,7149,7162,7166,7171,7178,7192,7202],{"__ignoreMap":196},[201,7080,7081],{"class":203,"line":204},[201,7082,7083],{"class":207},"#!\u002Fbin\u002Fbash\n",[201,7085,7086],{"class":203,"line":211},[201,7087,7088],{"class":207},"# build-standalone.sh\n",[201,7090,7091],{"class":203,"line":232},[201,7092,779],{"emptyLinePlaceholder":394},[201,7094,7095,7097],{"class":203,"line":245},[201,7096,1541],{"class":221},[201,7098,7099],{"class":221}," -e\n",[201,7101,7102],{"class":203,"line":256},[201,7103,779],{"emptyLinePlaceholder":394},[201,7105,7106],{"class":203,"line":267},[201,7107,7108],{"class":207},"# 1. 构建前端\n",[201,7110,7111,7114,7116],{"class":203,"line":273},[201,7112,7113],{"class":291},"pnpm",[201,7115,2290],{"class":238},[201,7117,7118],{"class":221}," --no-frozen-lockfile\n",[201,7120,7121,7123,7126],{"class":203,"line":819},[201,7122,366],{"class":291},[201,7124,7125],{"class":238}," vite",[201,7127,7128],{"class":238}," build\n",[201,7130,7131],{"class":203,"line":825},[201,7132,779],{"emptyLinePlaceholder":394},[201,7134,7135],{"class":203,"line":830},[201,7136,7137],{"class":207},"# 2. 复制到嵌入目录\n",[201,7139,7140,7143,7146],{"class":203,"line":836},[201,7141,7142],{"class":291},"rm",[201,7144,7145],{"class":221}," -rf",[201,7147,7148],{"class":238}," service\u002Fweb\u002Fdist\n",[201,7150,7151,7154,7157,7160],{"class":203,"line":853},[201,7152,7153],{"class":291},"cp",[201,7155,7156],{"class":221}," -r",[201,7158,7159],{"class":238}," dist",[201,7161,7148],{"class":238},[201,7163,7164],{"class":203,"line":858},[201,7165,779],{"emptyLinePlaceholder":394},[201,7167,7168],{"class":203,"line":864},[201,7169,7170],{"class":207},"# 3. 编译 Go 二进制\n",[201,7172,7173,7175],{"class":203,"line":903},[201,7174,1552],{"class":221},[201,7176,7177],{"class":238}," service\n",[201,7179,7180,7182,7184,7186,7189],{"class":203,"line":908},[201,7181,759],{"class":291},[201,7183,1625],{"class":238},[201,7185,1679],{"class":221},[201,7187,7188],{"class":238}," myapp.exe",[201,7190,7191],{"class":221}," \\\n",[201,7193,7194,7197,7200],{"class":203,"line":922},[201,7195,7196],{"class":221},"    -ldflags=",[201,7198,7199],{"class":238},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[201,7201,7191],{"class":221},[201,7203,7204],{"class":203,"line":931},[201,7205,7206],{"class":238},"    main.go\n",[12,7208,1742],{"id":1742},[564,7210,7212],{"id":7211},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[17,7214,7215,7216,7218,7219,7221],{},"首次实现时用了 Gin 的 ",[198,7217,7066],{}," 兜底，但部分静态资源返回 404。排查发现 ",[198,7220,7066],{}," 在某些路由未显式注册时不会按预期触发。",[17,7223,7224,7226,7227,2308,7230,7233,7234,7236],{},[21,7225,1825],{},"：显式注册 ",[198,7228,7229],{},"\u002Fassets\u002F*filepath",[198,7231,7232],{},"\u002Ffavicon.ico"," 等路由，",[198,7235,7066],{}," 仅作为兜底。",[564,7238,7240,7241,7243,7244,1764],{"id":7239},"坑二goembed-排除-_-开头文件","坑二：",[198,7242,554],{}," 排除 ",[198,7245,537],{},[17,7247,7248,7249,7252,7253,7255,7256,1739],{},"这是最坑的。Vite 构建产出了一个 ",[198,7250,7251],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[198,7254,554],{}," 当模式为目录时，",[21,7257,7258,7259,1125,7261,7263],{},"硬编码排除了 ",[198,7260,537],{},[198,7262,847],{}," 开头的文件",[191,7265,7267],{"className":757,"code":7266,"language":759,"meta":196,"style":196},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[198,7268,7269,7274],{"__ignoreMap":196},[201,7270,7271],{"class":203,"line":204},[201,7272,7273],{"class":207},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[201,7275,7276],{"class":203,"line":211},[201,7277,6734],{"class":207},[17,7279,7280,7281,1739],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[198,7282,7283],{},"Unexpected token '\u003C'",[17,7285,7286,7288,7289,7291],{},[21,7287,1825],{},"：改 Vite 配置，去除了 ",[198,7290,537],{}," 前缀：",[191,7293,7295],{"className":581,"code":7294,"language":583,"meta":196,"style":196},"\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",[198,7296,7297,7301,7308,7315,7322,7330,7373,7378,7382,7387],{"__ignoreMap":196},[201,7298,7299],{"class":203,"line":204},[201,7300,590],{"class":207},[201,7302,7303,7306],{"class":203,"line":211},[201,7304,7305],{"class":291},"build",[201,7307,736],{"class":228},[201,7309,7310,7313],{"class":203,"line":232},[201,7311,7312],{"class":291},"  rollupOptions",[201,7314,736],{"class":228},[201,7316,7317,7320],{"class":203,"line":245},[201,7318,7319],{"class":291},"    output",[201,7321,736],{"class":228},[201,7323,7324,7327],{"class":203,"line":256},[201,7325,7326],{"class":291},"      chunkFileNames",[201,7328,7329],{"class":228},"(chunkInfo) {\n",[201,7331,7332,7334,7337,7340,7342,7344,7346,7349,7351,7354,7357,7360,7362,7364,7367,7370],{"class":203,"line":267},[201,7333,4117],{"class":214},[201,7335,7336],{"class":238}," `assets\u002F${",[201,7338,7339],{"class":228},"chunkInfo",[201,7341,847],{"class":238},[201,7343,982],{"class":228},[201,7345,847],{"class":238},[201,7347,7348],{"class":291},"replace",[201,7350,979],{"class":238},[201,7352,7353],{"class":238},"\u002F",[201,7355,7356],{"class":214},"^",[201,7358,537],{"class":7359},"sPKmS",[201,7361,7353],{"class":238},[201,7363,338],{"class":238},[201,7365,7366],{"class":238},"''",[201,7368,7369],{"class":238},")",[201,7371,7372],{"class":238},"}-[hash].js`\n",[201,7374,7375],{"class":203,"line":273},[201,7376,7377],{"class":228},"      },\n",[201,7379,7380],{"class":203,"line":819},[201,7381,2897],{"class":228},[201,7383,7384],{"class":203,"line":825},[201,7385,7386],{"class":228},"  },\n",[201,7388,7389],{"class":203,"line":830},[201,7390,7391],{"class":228},"},\n",[17,7393,7394,7395,7397],{},"这样就避免产生 ",[198,7396,537],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[564,7399,7401],{"id":7400},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[17,7403,7404,7405,7407],{},"多次 ",[198,7406,1809],{}," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[17,7409,7410,7412,7413,7416],{},[21,7411,1825],{},"：使用 ",[198,7414,7415],{},"go build -a"," 强制重新编译所有包。",[12,7418,2013],{"id":2013},[191,7420,7422],{"className":357,"code":7421,"language":359,"meta":196,"style":196},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[198,7423,7424,7433,7444],{"__ignoreMap":196},[201,7425,7426,7428,7430],{"class":203,"line":204},[201,7427,2023],{"class":291},[201,7429,2036],{"class":238},[201,7431,7432],{"class":238}," build-standalone.sh\n",[201,7434,7435,7438,7441],{"class":203,"line":211},[201,7436,7437],{"class":238},"===",[201,7439,7440],{"class":238}," 构建成功",[201,7442,7443],{"class":238}," ===\n",[201,7445,7446,7449,7452],{"class":203,"line":232},[201,7447,7448],{"class":291},"输出:",[201,7450,7451],{"class":238}," service\u002Fmyapp.exe",[201,7453,7454],{"class":238},"  41M\n",[17,7456,7457],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[17,7459,7460],{},[21,7461,7462],{},"部署对比：",[35,7464,7465,7478],{},[38,7466,7467],{},[41,7468,7469,7472,7475],{},[44,7470,7471],{},"方式",[44,7473,7474],{},"文件数",[44,7476,7477],{},"操作",[51,7479,7480,7491],{},[41,7481,7482,7485,7488],{},[56,7483,7484],{},"旧方案",[56,7486,7487],{},"1 exe + 1 web 目录",[56,7489,7490],{},"解压→保持目录结构",[41,7492,7493,7496,7499],{},[56,7494,7495],{},"新方案",[56,7497,7498],{},"1 个 exe",[56,7500,7501],{},"直接扔到服务器运行",[12,7503,2054],{"id":2054},[17,7505,7506,7507,7509],{},"Go 的 ",[198,7508,554],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[509,7511,7512,7519],{},[124,7513,7514,7515,1125,7517,7263],{},"嵌入目录时自动排除 ",[198,7516,537],{},[198,7518,847],{},[124,7520,7521],{},"构建缓存可能导致 embed 不是最新的",[17,7523,7524],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[438,7526,7527],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sPKmS, html code.shiki .sPKmS{--shiki-light:#032F62;--shiki-dark:#DBEDFF}",{"title":196,"searchDepth":211,"depth":232,"links":7529},[7530,7531,7532,7537,7543,7544],{"id":473,"depth":211,"text":473},{"id":548,"depth":211,"text":548},{"id":562,"depth":211,"text":562,"children":7533},[7534,7535,7536],{"id":6630,"depth":232,"text":6631},{"id":6960,"depth":232,"text":6961},{"id":7072,"depth":232,"text":7073},{"id":1742,"depth":211,"text":1742,"children":7538},[7539,7540,7542],{"id":7211,"depth":232,"text":7212},{"id":7239,"depth":232,"text":7541},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":7400,"depth":232,"text":7401},{"id":2013,"depth":211,"text":2013},{"id":2054,"depth":211,"text":2054},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"2026-06-06T16:00:00",{"title":6600,"description":7545},"posts\u002Fgo-embed-frontend\u002Findex",[2126,7552,7553,7554],"Vue","编译","埋坑","2026-06-06","SIQNL24wVAFb18wIGWSB3lNVZUQ9B76jWgQ55TASTYk",{"id":7558,"title":7559,"author":7,"body":7560,"description":8320,"difficulty":7,"draft":452,"extension":453,"image":8321,"meta":8322,"navigation":394,"path":8323,"pinned":452,"published":8324,"seo":8325,"series":7,"seriesOrder":7,"stem":8326,"tags":8327,"updated":7,"__hash__":8329},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":9,"value":7561,"toc":8306},[7562,7566,7569,7576,7700,7702,7705,7760,7763,7777,7780,7783,7790,8023,8030,8034,8037,8057,8060,8075,8078,8097,8100,8103,8110,8176,8179,8181,8185,8188,8239,8243,8264,8268,8271,8285,8288,8292,8295,8297,8300,8303],[12,7563,7565],{"id":7564},"dws-是什么","dws 是什么",[17,7567,7568],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[17,7570,7571,7572,7575],{},"它的定位很明确：",[21,7573,7574],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[191,7577,7579],{"className":357,"code":7578,"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,7580,7581,7586,7611,7615,7620,7642,7646,7651,7675,7679,7684],{"__ignoreMap":196},[201,7582,7583],{"class":203,"line":204},[201,7584,7585],{"class":207},"# 查看今天有什么会\n",[201,7587,7588,7591,7594,7597,7599,7602,7605,7608],{"class":203,"line":211},[201,7589,7590],{"class":291},"dws",[201,7592,7593],{"class":238}," calendar",[201,7595,7596],{"class":238}," event",[201,7598,6182],{"class":238},[201,7600,7601],{"class":221}," --start",[201,7603,7604],{"class":238}," 2026-06-01",[201,7606,7607],{"class":221}," --end",[201,7609,7610],{"class":238}," 2026-06-01\n",[201,7612,7613],{"class":203,"line":232},[201,7614,779],{"emptyLinePlaceholder":394},[201,7616,7617],{"class":203,"line":245},[201,7618,7619],{"class":207},"# 给群里发条消息\n",[201,7621,7622,7624,7627,7630,7633,7636,7639],{"class":203,"line":256},[201,7623,7590],{"class":291},[201,7625,7626],{"class":238}," chat",[201,7628,7629],{"class":238}," send",[201,7631,7632],{"class":221}," --group-id",[201,7634,7635],{"class":238}," xxx",[201,7637,7638],{"class":221}," --content",[201,7640,7641],{"class":238}," \"今天发布推迟到下午3点\"\n",[201,7643,7644],{"class":203,"line":267},[201,7645,779],{"emptyLinePlaceholder":394},[201,7647,7648],{"class":203,"line":273},[201,7649,7650],{"class":207},"# 创建一个待办\n",[201,7652,7653,7655,7658,7661,7663,7666,7669,7672],{"class":203,"line":819},[201,7654,7590],{"class":291},[201,7656,7657],{"class":238}," todo",[201,7659,7660],{"class":238}," task",[201,7662,5634],{"class":238},[201,7664,7665],{"class":221}," --subject",[201,7667,7668],{"class":238}," \"Review PR #123\"",[201,7670,7671],{"class":221}," --due-date",[201,7673,7674],{"class":238}," 2026-06-02\n",[201,7676,7677],{"class":203,"line":825},[201,7678,779],{"emptyLinePlaceholder":394},[201,7680,7681],{"class":203,"line":830},[201,7682,7683],{"class":207},"# 读取钉钉文档\n",[201,7685,7686,7688,7691,7694,7697],{"class":203,"line":836},[201,7687,7590],{"class":291},[201,7689,7690],{"class":238}," doc",[201,7692,7693],{"class":238}," read",[201,7695,7696],{"class":221}," --url",[201,7698,7699],{"class":238}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[12,7701,5347],{"id":5347},[17,7703,7704],{},"三种方式，选你喜欢的：",[191,7706,7708],{"className":357,"code":7707,"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,7709,7710,7715,7727,7731,7736,7746,7750,7755],{"__ignoreMap":196},[201,7711,7712],{"class":203,"line":204},[201,7713,7714],{"class":207},"# npm（推荐，跨平台自动下载二进制）\n",[201,7716,7717,7719,7721,7724],{"class":203,"line":211},[201,7718,1619],{"class":291},[201,7720,2290],{"class":238},[201,7722,7723],{"class":221}," -g",[201,7725,7726],{"class":238}," dingtalk-workspace-cli\n",[201,7728,7729],{"class":203,"line":232},[201,7730,779],{"emptyLinePlaceholder":394},[201,7732,7733],{"class":203,"line":245},[201,7734,7735],{"class":207},"# Homebrew（macOS\u002FLinux）\n",[201,7737,7738,7741,7743],{"class":203,"line":256},[201,7739,7740],{"class":291},"brew",[201,7742,2290],{"class":238},[201,7744,7745],{"class":238}," dws\n",[201,7747,7748],{"class":203,"line":267},[201,7749,779],{"emptyLinePlaceholder":394},[201,7751,7752],{"class":203,"line":273},[201,7753,7754],{"class":207},"# 直接下载二进制\n",[201,7756,7757],{"class":203,"line":819},[201,7758,7759],{"class":207},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[17,7761,7762],{},"安装后认证：",[191,7764,7766],{"className":357,"code":7765,"language":359,"meta":196,"style":196},"dws auth login\n",[198,7767,7768],{"__ignoreMap":196},[201,7769,7770,7772,7774],{"class":203,"line":204},[201,7771,7590],{"class":291},[201,7773,5469],{"class":238},[201,7775,7776],{"class":238}," login\n",[17,7778,7779],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[12,7781,7782],{"id":7782},"覆盖的钉钉产品",[17,7784,7785,7786,7789],{},"这是最夸张的部分 — ",[21,7787,7788],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[35,7791,7792,7804],{},[38,7793,7794],{},[41,7795,7796,7799,7802],{},[44,7797,7798],{},"产品",[44,7800,7801],{},"命令数",[44,7803,2185],{},[51,7805,7806,7820,7834,7848,7862,7876,7890,7904,7917,7931,7944,7958,7972,7985,7998,8012],{},[41,7807,7808,7814,7817],{},[56,7809,7810,7811],{},"聊天 ",[198,7812,7813],{},"chat",[56,7815,7816],{},"57",[56,7818,7819],{},"消息收发、群管理、机器人、已读状态、Webhook",[41,7821,7822,7828,7831],{},[56,7823,7824,7825],{},"AI 表格 ",[198,7826,7827],{},"aitable",[56,7829,7830],{},"42",[56,7832,7833],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[41,7835,7836,7842,7845],{},[56,7837,7838,7839],{},"电子表格 ",[198,7840,7841],{},"sheet",[56,7843,7844],{},"34",[56,7846,7847],{},"工作表读写、行列操作、合并、筛选视图、导出",[41,7849,7850,7856,7859],{},[56,7851,7852,7853],{},"文档 ",[198,7854,7855],{},"doc",[56,7857,7858],{},"21",[56,7860,7861],{},"搜索、读写、块级编辑、评论、复制移动",[41,7863,7864,7870,7873],{},[56,7865,7866,7867],{},"会议纪要 ",[198,7868,7869],{},"minutes",[56,7871,7872],{},"19",[56,7874,7875],{},"列表、摘要、转写、思维导图、发言人、热词",[41,7877,7878,7884,7887],{},[56,7879,7880,7881],{},"日历 ",[198,7882,7883],{},"calendar",[56,7885,7886],{},"14",[56,7888,7889],{},"事件 CRUD、会议室、忙闲查询、时间建议",[41,7891,7892,7898,7901],{},[56,7893,7894,7895],{},"审批 ",[198,7896,7897],{},"oa",[56,7899,7900],{},"9",[56,7902,7903],{},"待办、发起、同意\u002F拒绝\u002F撤销",[41,7905,7906,7912,7914],{},[56,7907,7908,7909],{},"云盘 ",[198,7910,7911],{},"drive",[56,7913,7900],{},[56,7915,7916],{},"列表、下载、上传、创建目录",[41,7918,7919,7925,7928],{},[56,7920,7921,7922],{},"知识库 ",[198,7923,7924],{},"wiki",[56,7926,7927],{},"7",[56,7929,7930],{},"空间管理、成员管理",[41,7932,7933,7939,7941],{},[56,7934,7935,7936],{},"汇报 ",[198,7937,7938],{},"report",[56,7940,7927],{},[56,7942,7943],{},"日志\u002F日报\u002F周报，按模板创建、统计",[41,7945,7946,7952,7955],{},[56,7947,7948,7949],{},"待办 ",[198,7950,7951],{},"todo",[56,7953,7954],{},"6",[56,7956,7957],{},"创建、完成、删除，支持优先级和循环",[41,7959,7960,7966,7969],{},[56,7961,7962,7963],{},"邮件 ",[198,7964,7965],{},"mail",[56,7967,7968],{},"4",[56,7970,7971],{},"邮箱查询、邮件搜索、发送",[41,7973,7974,7980,7982],{},[56,7975,7976,7977],{},"考勤 ",[198,7978,7979],{},"attendance",[56,7981,7968],{},[56,7983,7984],{},"打卡记录、排班、汇总",[41,7986,7987,7993,7995],{},[56,7988,7989,7990],{},"通讯录 ",[198,7991,7992],{},"contact",[56,7994,3721],{},[56,7996,7997],{},"用户搜索、部门查询",[41,7999,8000,8006,8009],{},[56,8001,8002,8003],{},"DING 消息 ",[198,8004,8005],{},"ding",[56,8007,8008],{},"2",[56,8010,8011],{},"发送\u002F撤回（应用内、短信、电话）",[41,8013,8014,8017,8020],{},[56,8015,8016],{},"其他",[56,8018,8019],{},"若干",[56,8021,8022],{},"AI 搜索、AI 应用、直播、开放平台文档",[17,8024,8025,8026,8029],{},"还有一个万能命令 ",[198,8027,8028],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[12,8031,8033],{"id":8032},"agent-skill-系统","Agent Skill 系统",[17,8035,8036],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[121,8038,8039,8045,8051],{},[124,8040,8041,8044],{},[21,8042,8043],{},"Claude Code"," — 通过 SKILL.md 自动加载",[124,8046,8047,8050],{},[21,8048,8049],{},"Cursor"," — 支持 MCP 协议接入",[124,8052,8053,8056],{},[21,8054,8055],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[17,8058,8059],{},"安装 skill：",[191,8061,8063],{"className":357,"code":8062,"language":359,"meta":196,"style":196},"dws skill setup\n",[198,8064,8065],{"__ignoreMap":196},[201,8066,8067,8069,8072],{"class":203,"line":204},[201,8068,7590],{"class":291},[201,8070,8071],{"class":238}," skill",[201,8073,8074],{"class":238}," setup\n",[17,8076,8077],{},"SKILL.md 里定义了：",[121,8079,8080,8088,8091,8094],{},[124,8081,8082,8083,8085,8086,497],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[198,8084,7827],{},"，说\"打卡\"→ ",[198,8087,7979],{},[124,8089,8090],{},"危险操作确认流程（删除、撤回等必须先确认）",[124,8092,8093],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[124,8095,8096],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[17,8098,8099],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[12,8101,8102],{"id":8102},"自动升级",[17,8104,8105,8106,8109],{},"内置 ",[198,8107,8108],{},"dws upgrade","，不需要重新下载安装包：",[191,8111,8113],{"className":357,"code":8112,"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,8114,8115,8125,8137,8149,8164],{"__ignoreMap":196},[201,8116,8117,8119,8122],{"class":203,"line":204},[201,8118,7590],{"class":291},[201,8120,8121],{"class":238}," upgrade",[201,8123,8124],{"class":207},"              # 交互式升级到最新版\n",[201,8126,8127,8129,8131,8134],{"class":203,"line":211},[201,8128,7590],{"class":291},[201,8130,8121],{"class":238},[201,8132,8133],{"class":221}," --check",[201,8135,8136],{"class":207},"      # 仅检查是否有新版本\n",[201,8138,8139,8141,8143,8146],{"class":203,"line":232},[201,8140,7590],{"class":291},[201,8142,8121],{"class":238},[201,8144,8145],{"class":221}," --list",[201,8147,8148],{"class":207},"       # 列出最近版本\n",[201,8150,8151,8153,8155,8158,8161],{"class":203,"line":245},[201,8152,7590],{"class":291},[201,8154,8121],{"class":238},[201,8156,8157],{"class":221}," --version",[201,8159,8160],{"class":238}," v1.0.5",[201,8162,8163],{"class":207},"  # 升级到指定版本\n",[201,8165,8166,8168,8170,8173],{"class":203,"line":256},[201,8167,7590],{"class":291},[201,8169,8121],{"class":238},[201,8171,8172],{"class":221}," --rollback",[201,8174,8175],{"class":207},"   # 回滚到上一版本\n",[17,8177,8178],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[12,8180,5794],{"id":5794},[564,8182,8184],{"id":8183},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[17,8186,8187],{},"这可能是目前对 AI 最友好的 CLI 工具：",[121,8189,8190,8210,8220,8229],{},[124,8191,8192,8195,8196,8199,8200,1851,8203,8199,8206,8209],{},[21,8193,8194],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[198,8197,8198],{},"--userId"," 应该是 ",[198,8201,8202],{},"--user-id",[198,8204,8205],{},"--limit100",[198,8207,8208],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[124,8211,8212,8215,8216,8219],{},[21,8213,8214],{},"Schema 发现"," — ",[198,8217,8218],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[124,8221,8222,8215,8225,8228],{},[21,8223,8224],{},"jq 过滤",[198,8226,8227],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[124,8230,8231,8234,8235,8238],{},[21,8232,8233],{},"JSON 输出"," — 所有命令支持 ",[198,8236,8237],{},"--format json","，方便程序解析",[564,8240,8242],{"id":8241},"_2-安全设计","2. 安全设计",[121,8244,8245,8248,8251,8254,8261],{},[124,8246,8247],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[124,8249,8250],{},"域名白名单，只能访问钉钉官方 API",[124,8252,8253],{},"强制 HTTPS",[124,8255,8256,8257,8260],{},"写操作必须显式 ",[198,8258,8259],{},"--yes"," 确认",[124,8262,8263],{},"完整审计链",[564,8265,8267],{"id":8266},"_3-发现驱动架构","3. 发现驱动架构",[17,8269,8270],{},"不是把所有命令硬编码在代码里，而是：",[509,8272,8273,8276,8279,8282],{},[124,8274,8275],{},"从钉钉 MCP 服务器注册中心拉取元数据",[124,8277,8278],{},"运行时发现服务端能力",[124,8280,8281],{},"转换成统一的中间表示（IR）",[124,8283,8284],{},"动态挂载到命令树",[17,8286,8287],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[564,8289,8291],{"id":8290},"_4-多平台分发","4. 多平台分发",[17,8293,8294],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[12,8296,2054],{"id":2054},[17,8298,8299],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[17,8301,8302],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[438,8304,8305],{},"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":8307},[8308,8309,8310,8311,8312,8313,8319],{"id":7564,"depth":211,"text":7565},{"id":5347,"depth":211,"text":5347},{"id":7782,"depth":211,"text":7782},{"id":8032,"depth":211,"text":8033},{"id":8102,"depth":211,"text":8102},{"id":5794,"depth":211,"text":5794,"children":8314},[8315,8316,8317,8318],{"id":8183,"depth":232,"text":8184},{"id":8241,"depth":232,"text":8242},{"id":8266,"depth":232,"text":8267},{"id":8290,"depth":232,"text":8291},{"id":2054,"depth":211,"text":2054},"介绍阿里巴巴官方的 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":7559,"description":8320},"posts\u002Fdws-dingtalk-cli\u002Findex",[8328,6593,6595,2126,6596],"钉钉","4ca65VUZ2Sz9yG8dNtftrBDDHE6QowA3ed9g4egQkKI",{"id":8331,"title":8332,"author":7,"body":8333,"description":10338,"difficulty":2117,"draft":452,"extension":453,"image":10339,"meta":10340,"navigation":394,"path":10341,"pinned":452,"published":10342,"seo":10343,"series":2123,"seriesOrder":204,"stem":10344,"tags":10345,"updated":10346,"__hash__":10347},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",{"type":9,"value":8334,"toc":10319},[8335,8339,8342,8345,8349,8356,8362,8365,8368,8388,8391,8395,8418,8422,8519,8532,8536,8693,8696,8909,8913,8924,9023,9030,9034,9298,9301,9381,9385,9388,9390,9401,9405,9411,9767,9953,9956,10012,10015,10168,10171,10174,10241,10243,10307,10316],[12,8336,8338],{"id":8337},"什么是-mcp","什么是 MCP",[17,8340,8341],{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[17,8343,8344],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[12,8346,8348],{"id":8347},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[17,8350,8351,8352,8355],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[198,8353,8354],{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[17,8357,8358,8359],{},"核心思路：",[21,8360,8361],{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[12,8363,8364],{"id":8364},"需求分析",[17,8366,8367],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[509,8369,8370,8376,8382],{},[124,8371,8372,8375],{},[21,8373,8374],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[124,8377,8378,8381],{},[21,8379,8380],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[124,8383,8384,8387],{},[21,8385,8386],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[12,8389,8390],{"id":8390},"实现步骤",[564,8392,8394],{"id":8393},"_1-安装依赖","1. 安装依赖",[191,8396,8398],{"className":357,"code":8397,"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,8399,8400,8409],{"__ignoreMap":196},[201,8401,8402,8404,8406],{"class":203,"line":204},[201,8403,759],{"class":291},[201,8405,5225],{"class":238},[201,8407,8408],{"class":238}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[201,8410,8411,8413,8415],{"class":203,"line":211},[201,8412,759],{"class":291},[201,8414,5225],{"class":238},[201,8416,8417],{"class":238}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[564,8419,8421],{"id":8420},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[191,8423,8425],{"className":757,"code":8424,"language":759,"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,8426,8427,8438,8442,8458,8469,8480,8491,8500,8515],{"__ignoreMap":196},[201,8428,8429,8431,8433,8436],{"class":203,"line":204},[201,8430,784],{"class":214},[201,8432,1561],{"class":238},[201,8434,8435],{"class":291},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[201,8437,798],{"class":238},[201,8439,8440],{"class":203,"line":211},[201,8441,779],{"emptyLinePlaceholder":394},[201,8443,8444,8447,8449,8452,8455],{"class":203,"line":232},[201,8445,8446],{"class":228},"mcpServer ",[201,8448,1214],{"class":214},[201,8450,8451],{"class":228}," server.",[201,8453,8454],{"class":291},"NewMCPServer",[201,8456,8457],{"class":228},"(\n",[201,8459,8460,8463,8466],{"class":203,"line":245},[201,8461,8462],{"class":238},"    \"my-app\"",[201,8464,8465],{"class":228},",    ",[201,8467,8468],{"class":207},"\u002F\u002F 服务器名称\n",[201,8470,8471,8474,8477],{"class":203,"line":256},[201,8472,8473],{"class":238},"    \"1.0.0\"",[201,8475,8476],{"class":228},",     ",[201,8478,8479],{"class":207},"\u002F\u002F 版本\n",[201,8481,8482,8485,8488],{"class":203,"line":267},[201,8483,8484],{"class":228},"    server.",[201,8486,8487],{"class":291},"WithLogging",[201,8489,8490],{"class":228},"(),\n",[201,8492,8493,8495,8498],{"class":203,"line":273},[201,8494,8484],{"class":228},[201,8496,8497],{"class":291},"WithRecovery",[201,8499,8490],{"class":228},[201,8501,8502,8504,8507,8509,8512],{"class":203,"line":819},[201,8503,8484],{"class":228},[201,8505,8506],{"class":291},"WithInstructions",[201,8508,979],{"class":228},[201,8510,8511],{"class":238},"\"这是一个 MCP 服务器示例\"",[201,8513,8514],{"class":228},"),\n",[201,8516,8517],{"class":203,"line":825},[201,8518,822],{"class":228},[17,8520,8521,8523,8524,8527,8528,8531],{},[198,8522,8454],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[198,8525,8526],{},"tools\u002Flist","）和工具调用（",[198,8529,8530],{},"tools\u002Fcall","）的协议逻辑。",[564,8533,8535],{"id":8534},"_3-注册工具","3. 注册工具",[191,8537,8539],{"className":757,"code":8538,"language":759,"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,8540,8541,8552,8556,8561,8581,8596,8600,8604,8609,8664,8669,8689],{"__ignoreMap":196},[201,8542,8543,8545,8547,8550],{"class":203,"line":204},[201,8544,784],{"class":214},[201,8546,1561],{"class":238},[201,8548,8549],{"class":291},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[201,8551,798],{"class":238},[201,8553,8554],{"class":203,"line":211},[201,8555,779],{"emptyLinePlaceholder":394},[201,8557,8558],{"class":203,"line":232},[201,8559,8560],{"class":207},"\u002F\u002F 定义工具\n",[201,8562,8563,8566,8568,8571,8574,8576,8579],{"class":203,"line":245},[201,8564,8565],{"class":228},"listTool ",[201,8567,1214],{"class":214},[201,8569,8570],{"class":228}," mcp.",[201,8572,8573],{"class":291},"NewTool",[201,8575,979],{"class":228},[201,8577,8578],{"class":238},"\"list_items\"",[201,8580,242],{"class":228},[201,8582,8583,8586,8589,8591,8594],{"class":203,"line":256},[201,8584,8585],{"class":228},"    mcp.",[201,8587,8588],{"class":291},"WithDescription",[201,8590,979],{"class":228},[201,8592,8593],{"class":238},"\"列出所有项目\"",[201,8595,8514],{"class":228},[201,8597,8598],{"class":203,"line":267},[201,8599,822],{"class":228},[201,8601,8602],{"class":203,"line":273},[201,8603,779],{"emptyLinePlaceholder":394},[201,8605,8606],{"class":203,"line":819},[201,8607,8608],{"class":207},"\u002F\u002F 注册处理器\n",[201,8610,8611,8614,8617,8620,8622,8624,8627,8630,8632,8634,8636,8639,8641,8643,8646,8648,8650,8653,8655,8658,8660,8662],{"class":203,"line":825},[201,8612,8613],{"class":228},"mcpServer.",[201,8615,8616],{"class":291},"AddTool",[201,8618,8619],{"class":228},"(listTool, ",[201,8621,959],{"class":214},[201,8623,979],{"class":228},[201,8625,8626],{"class":965},"ctx",[201,8628,8629],{"class":291}," context",[201,8631,847],{"class":228},[201,8633,1274],{"class":291},[201,8635,338],{"class":228},[201,8637,8638],{"class":965},"req",[201,8640,5259],{"class":291},[201,8642,847],{"class":228},[201,8644,8645],{"class":291},"CallToolRequest",[201,8647,988],{"class":228},[201,8649,937],{"class":214},[201,8651,8652],{"class":291},"mcp",[201,8654,847],{"class":228},[201,8656,8657],{"class":291},"CallToolResult",[201,8659,338],{"class":228},[201,8661,1000],{"class":214},[201,8663,1003],{"class":228},[201,8665,8666],{"class":203,"line":830},[201,8667,8668],{"class":207},"    \u002F\u002F 业务逻辑\n",[201,8670,8671,8673,8675,8678,8680,8683,8686],{"class":203,"line":836},[201,8672,1087],{"class":214},[201,8674,8570],{"class":228},[201,8676,8677],{"class":291},"NewToolResultText",[201,8679,979],{"class":228},[201,8681,8682],{"class":238},"\"返回结果\"",[201,8684,8685],{"class":228},"), ",[201,8687,8688],{"class":221},"nil\n",[201,8690,8691],{"class":203,"line":853},[201,8692,627],{"class":228},[17,8694,8695],{},"带参数的工具：",[191,8697,8699],{"className":757,"code":8698,"language":759,"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,8700,8701,8719,8732,8746,8756,8770,8775,8779,8783,8830,8849,8861,8879,8883,8888,8905],{"__ignoreMap":196},[201,8702,8703,8706,8708,8710,8712,8714,8717],{"class":203,"line":204},[201,8704,8705],{"class":228},"getTool ",[201,8707,1214],{"class":214},[201,8709,8570],{"class":228},[201,8711,8573],{"class":291},[201,8713,979],{"class":228},[201,8715,8716],{"class":238},"\"get_item\"",[201,8718,242],{"class":228},[201,8720,8721,8723,8725,8727,8730],{"class":203,"line":211},[201,8722,8585],{"class":228},[201,8724,8588],{"class":291},[201,8726,979],{"class":228},[201,8728,8729],{"class":238},"\"获取指定项目的详情\"",[201,8731,8514],{"class":228},[201,8733,8734,8736,8739,8741,8744],{"class":203,"line":232},[201,8735,8585],{"class":228},[201,8737,8738],{"class":291},"WithString",[201,8740,979],{"class":228},[201,8742,8743],{"class":238},"\"item_id\"",[201,8745,242],{"class":228},[201,8747,8748,8751,8754],{"class":203,"line":245},[201,8749,8750],{"class":228},"        mcp.",[201,8752,8753],{"class":291},"Required",[201,8755,8490],{"class":228},[201,8757,8758,8760,8763,8765,8768],{"class":203,"line":256},[201,8759,8750],{"class":228},[201,8761,8762],{"class":291},"Description",[201,8764,979],{"class":228},[201,8766,8767],{"class":238},"\"项目 ID\"",[201,8769,8514],{"class":228},[201,8771,8772],{"class":203,"line":267},[201,8773,8774],{"class":228},"    ),\n",[201,8776,8777],{"class":203,"line":273},[201,8778,822],{"class":228},[201,8780,8781],{"class":203,"line":819},[201,8782,779],{"emptyLinePlaceholder":394},[201,8784,8785,8787,8789,8792,8794,8796,8798,8800,8802,8804,8806,8808,8810,8812,8814,8816,8818,8820,8822,8824,8826,8828],{"class":203,"line":825},[201,8786,8613],{"class":228},[201,8788,8616],{"class":291},[201,8790,8791],{"class":228},"(getTool, ",[201,8793,959],{"class":214},[201,8795,979],{"class":228},[201,8797,8626],{"class":965},[201,8799,8629],{"class":291},[201,8801,847],{"class":228},[201,8803,1274],{"class":291},[201,8805,338],{"class":228},[201,8807,8638],{"class":965},[201,8809,5259],{"class":291},[201,8811,847],{"class":228},[201,8813,8645],{"class":291},[201,8815,988],{"class":228},[201,8817,937],{"class":214},[201,8819,8652],{"class":291},[201,8821,847],{"class":228},[201,8823,8657],{"class":291},[201,8825,338],{"class":228},[201,8827,1000],{"class":214},[201,8829,1003],{"class":228},[201,8831,8832,8835,8837,8840,8843,8845,8847],{"class":203,"line":830},[201,8833,8834],{"class":228},"    itemID, err ",[201,8836,1214],{"class":214},[201,8838,8839],{"class":228}," req.",[201,8841,8842],{"class":291},"RequireString",[201,8844,979],{"class":228},[201,8846,8743],{"class":238},[201,8848,822],{"class":228},[201,8850,8851,8853,8855,8857,8859],{"class":203,"line":836},[201,8852,1009],{"class":214},[201,8854,3790],{"class":228},[201,8856,3793],{"class":214},[201,8858,3796],{"class":221},[201,8860,229],{"class":228},[201,8862,8863,8865,8867,8870,8872,8875,8877],{"class":203,"line":853},[201,8864,4117],{"class":214},[201,8866,8570],{"class":228},[201,8868,8869],{"class":291},"NewToolResultError",[201,8871,979],{"class":228},[201,8873,8874],{"class":238},"\"缺少 item_id 参数\"",[201,8876,8685],{"class":228},[201,8878,8688],{"class":221},[201,8880,8881],{"class":203,"line":858},[201,8882,1081],{"class":228},[201,8884,8885],{"class":203,"line":864},[201,8886,8887],{"class":207},"    \u002F\u002F 查询并返回\n",[201,8889,8890,8892,8894,8896,8898,8901,8903],{"class":203,"line":903},[201,8891,1087],{"class":214},[201,8893,8570],{"class":228},[201,8895,8677],{"class":291},[201,8897,979],{"class":228},[201,8899,8900],{"class":238},"\"项目详情...\"",[201,8902,8685],{"class":228},[201,8904,8688],{"class":221},[201,8906,8907],{"class":203,"line":908},[201,8908,627],{"class":228},[564,8910,8912],{"id":8911},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[17,8914,8915,8916,8919,8920,8923],{},"关键一步：用 ",[198,8917,8918],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[198,8921,8922],{},"gin.WrapH"," 适配 Gin。",[191,8925,8927],{"className":757,"code":8926,"language":759,"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,8928,8929,8939,8943,8958,8972,8989,8993,8997,9002],{"__ignoreMap":196},[201,8930,8931,8933,8935,8937],{"class":203,"line":204},[201,8932,784],{"class":214},[201,8934,1561],{"class":238},[201,8936,8435],{"class":291},[201,8938,798],{"class":238},[201,8940,8941],{"class":203,"line":211},[201,8942,779],{"emptyLinePlaceholder":394},[201,8944,8945,8948,8950,8952,8955],{"class":203,"line":232},[201,8946,8947],{"class":228},"mcpHTTPServer ",[201,8949,1214],{"class":214},[201,8951,8451],{"class":228},[201,8953,8954],{"class":291},"NewStreamableHTTPServer",[201,8956,8957],{"class":228},"(mcpServer,\n",[201,8959,8960,8962,8965,8967,8970],{"class":203,"line":245},[201,8961,8484],{"class":228},[201,8963,8964],{"class":291},"WithEndpointPath",[201,8966,979],{"class":228},[201,8968,8969],{"class":238},"\"\u002Fmcp\"",[201,8971,8514],{"class":228},[201,8973,8974,8976,8979,8981,8984,8986],{"class":203,"line":256},[201,8975,8484],{"class":228},[201,8977,8978],{"class":291},"WithHeartbeatInterval",[201,8980,979],{"class":228},[201,8982,8983],{"class":221},"30",[201,8985,937],{"class":214},[201,8987,8988],{"class":228},"time.Second),\n",[201,8990,8991],{"class":203,"line":267},[201,8992,822],{"class":228},[201,8994,8995],{"class":203,"line":273},[201,8996,779],{"emptyLinePlaceholder":394},[201,8998,8999],{"class":203,"line":819},[201,9000,9001],{"class":207},"\u002F\u002F 挂载到 Gin\n",[201,9003,9004,9007,9010,9012,9014,9017,9020],{"class":203,"line":825},[201,9005,9006],{"class":228},"r.",[201,9008,9009],{"class":291},"Any",[201,9011,979],{"class":228},[201,9013,8969],{"class":238},[201,9015,9016],{"class":228},", gin.",[201,9018,9019],{"class":291},"WrapH",[201,9021,9022],{"class":228},"(mcpHTTPServer))\n",[17,9024,9025,9026,9029],{},"就这样，",[198,9027,9028],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[564,9031,9033],{"id":9032},"_5-完整的服务启动代码","5. 完整的服务启动代码",[191,9035,9037],{"className":757,"code":9036,"language":759,"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,9038,9039,9049,9061,9065,9070,9093,9102,9110,9115,9119,9124,9132,9136,9141,9154,9166,9180,9184,9201,9205,9210,9239,9272,9276,9280,9294],{"__ignoreMap":196},[201,9040,9041,9043,9046],{"class":203,"line":204},[201,9042,959],{"class":214},[201,9044,9045],{"class":291}," main",[201,9047,9048],{"class":228},"() {\n",[201,9050,9051,9053,9055,9057,9059],{"class":203,"line":211},[201,9052,1211],{"class":228},[201,9054,1214],{"class":214},[201,9056,1217],{"class":228},[201,9058,1220],{"class":291},[201,9060,1223],{"class":228},[201,9062,9063],{"class":203,"line":232},[201,9064,779],{"emptyLinePlaceholder":394},[201,9066,9067],{"class":203,"line":245},[201,9068,9069],{"class":207},"    \u002F\u002F 创建 MCP 服务器\n",[201,9071,9072,9075,9077,9079,9081,9083,9086,9088,9091],{"class":203,"line":256},[201,9073,9074],{"class":228},"    mcpServer ",[201,9076,1214],{"class":214},[201,9078,8451],{"class":228},[201,9080,8454],{"class":291},[201,9082,979],{"class":228},[201,9084,9085],{"class":238},"\"my-app\"",[201,9087,338],{"class":228},[201,9089,9090],{"class":238},"\"1.0.0\"",[201,9092,242],{"class":228},[201,9094,9095,9098,9100],{"class":203,"line":267},[201,9096,9097],{"class":228},"        server.",[201,9099,8487],{"class":291},[201,9101,8490],{"class":228},[201,9103,9104,9106,9108],{"class":203,"line":273},[201,9105,9097],{"class":228},[201,9107,8497],{"class":291},[201,9109,8490],{"class":228},[201,9111,9112],{"class":203,"line":819},[201,9113,9114],{"class":228},"    )\n",[201,9116,9117],{"class":203,"line":825},[201,9118,779],{"emptyLinePlaceholder":394},[201,9120,9121],{"class":203,"line":830},[201,9122,9123],{"class":207},"    \u002F\u002F 注册工具\n",[201,9125,9126,9129],{"class":203,"line":836},[201,9127,9128],{"class":291},"    registerTools",[201,9130,9131],{"class":228},"(mcpServer)\n",[201,9133,9134],{"class":203,"line":853},[201,9135,779],{"emptyLinePlaceholder":394},[201,9137,9138],{"class":203,"line":858},[201,9139,9140],{"class":207},"    \u002F\u002F 挂载到 Gin\n",[201,9142,9143,9146,9148,9150,9152],{"class":203,"line":864},[201,9144,9145],{"class":228},"    mcpHTTP ",[201,9147,1214],{"class":214},[201,9149,8451],{"class":228},[201,9151,8954],{"class":291},[201,9153,8957],{"class":228},[201,9155,9156,9158,9160,9162,9164],{"class":203,"line":903},[201,9157,9097],{"class":228},[201,9159,8964],{"class":291},[201,9161,979],{"class":228},[201,9163,8969],{"class":238},[201,9165,8514],{"class":228},[201,9167,9168,9170,9172,9174,9176,9178],{"class":203,"line":908},[201,9169,9097],{"class":228},[201,9171,8978],{"class":291},[201,9173,979],{"class":228},[201,9175,8983],{"class":221},[201,9177,937],{"class":214},[201,9179,8988],{"class":228},[201,9181,9182],{"class":203,"line":922},[201,9183,9114],{"class":228},[201,9185,9186,9188,9190,9192,9194,9196,9198],{"class":203,"line":931},[201,9187,1248],{"class":228},[201,9189,9009],{"class":291},[201,9191,979],{"class":228},[201,9193,8969],{"class":238},[201,9195,9016],{"class":228},[201,9197,9019],{"class":291},[201,9199,9200],{"class":228},"(mcpHTTP))\n",[201,9202,9203],{"class":203,"line":946},[201,9204,779],{"emptyLinePlaceholder":394},[201,9206,9207],{"class":203,"line":951},[201,9208,9209],{"class":207},"    \u002F\u002F 其他业务路由\n",[201,9211,9212,9214,9216,9218,9221,9223,9225,9227,9229,9231,9233,9235,9237],{"class":203,"line":956},[201,9213,1248],{"class":228},[201,9215,1251],{"class":291},[201,9217,979],{"class":228},[201,9219,9220],{"class":238},"\"\u002Fapi\u002Fhealth\"",[201,9222,338],{"class":228},[201,9224,959],{"class":214},[201,9226,979],{"class":228},[201,9228,1265],{"class":965},[201,9230,1177],{"class":214},[201,9232,1199],{"class":291},[201,9234,847],{"class":228},[201,9236,1274],{"class":291},[201,9238,1003],{"class":228},[201,9240,9241,9243,9246,9248,9251,9253,9255,9257,9260,9262,9265,9267,9270],{"class":203,"line":1006},[201,9242,6929],{"class":228},[201,9244,9245],{"class":291},"JSON",[201,9247,979],{"class":228},[201,9249,9250],{"class":221},"200",[201,9252,338],{"class":228},[201,9254,1199],{"class":291},[201,9256,847],{"class":228},[201,9258,9259],{"class":291},"H",[201,9261,2605],{"class":228},[201,9263,9264],{"class":238},"\"status\"",[201,9266,301],{"class":228},[201,9268,9269],{"class":238},"\"ok\"",[201,9271,627],{"class":228},[201,9273,9274],{"class":203,"line":1023},[201,9275,1466],{"class":228},[201,9277,9278],{"class":203,"line":1035},[201,9279,779],{"emptyLinePlaceholder":394},[201,9281,9282,9284,9287,9289,9292],{"class":203,"line":1046},[201,9283,1248],{"class":228},[201,9285,9286],{"class":291},"Run",[201,9288,979],{"class":228},[201,9290,9291],{"class":238},"\":8080\"",[201,9293,822],{"class":228},[201,9295,9296],{"class":203,"line":1078},[201,9297,276],{"class":228},[17,9299,9300],{},"此时启动服务，用 curl 测试：",[191,9302,9304],{"className":357,"code":9303,"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,9305,9306,9311,9327,9337,9345,9349,9354,9366,9374],{"__ignoreMap":196},[201,9307,9308],{"class":203,"line":204},[201,9309,9310],{"class":207},"# 发现工具列表\n",[201,9312,9313,9316,9319,9322,9325],{"class":203,"line":211},[201,9314,9315],{"class":291},"curl",[201,9317,9318],{"class":221}," -X",[201,9320,9321],{"class":238}," POST",[201,9323,9324],{"class":238}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[201,9326,7191],{"class":221},[201,9328,9329,9332,9335],{"class":203,"line":232},[201,9330,9331],{"class":221},"  -H",[201,9333,9334],{"class":238}," \"Content-Type: application\u002Fjson\"",[201,9336,7191],{"class":221},[201,9338,9339,9342],{"class":203,"line":245},[201,9340,9341],{"class":221},"  -d",[201,9343,9344],{"class":238}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[201,9346,9347],{"class":203,"line":256},[201,9348,779],{"emptyLinePlaceholder":394},[201,9350,9351],{"class":203,"line":267},[201,9352,9353],{"class":207},"# 调用工具\n",[201,9355,9356,9358,9360,9362,9364],{"class":203,"line":273},[201,9357,9315],{"class":291},[201,9359,9318],{"class":221},[201,9361,9321],{"class":238},[201,9363,9324],{"class":238},[201,9365,7191],{"class":221},[201,9367,9368,9370,9372],{"class":203,"line":819},[201,9369,9331],{"class":221},[201,9371,9334],{"class":238},[201,9373,7191],{"class":221},[201,9375,9376,9378],{"class":203,"line":825},[201,9377,9341],{"class":221},[201,9379,9380],{"class":238}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[12,9382,9384],{"id":9383},"拓展接入鉴权","拓展：接入鉴权",[17,9386,9387],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[564,9389,4710],{"id":4710},[17,9391,9392,9393,9396,9397,9400],{},"MCP 的工具处理器签名为 ",[198,9394,9395],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[198,9398,9399],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[564,9402,9404],{"id":9403},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[17,9406,9407,9408,1739],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[198,9409,9410],{},"request.Context",[191,9412,9414],{"className":757,"code":9413,"language":759,"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,9415,9416,9421,9448,9468,9487,9500,9532,9537,9541,9545,9569,9607,9636,9640,9644,9648,9668,9680,9709,9713,9717,9721,9736,9750,9759,9763],{"__ignoreMap":196},[201,9417,9418],{"class":203,"line":204},[201,9419,9420],{"class":207},"\u002F\u002F Gin 鉴权中间件\n",[201,9422,9423,9425,9428,9430,9433,9435,9438,9440,9442,9444,9446],{"class":203,"line":211},[201,9424,959],{"class":214},[201,9426,9427],{"class":291}," AuthRequired",[201,9429,979],{"class":228},[201,9431,9432],{"class":965},"authService",[201,9434,1177],{"class":214},[201,9436,9437],{"class":291},"AuthService",[201,9439,973],{"class":228},[201,9441,1199],{"class":291},[201,9443,847],{"class":228},[201,9445,6767],{"class":291},[201,9447,229],{"class":228},[201,9449,9450,9452,9454,9456,9458,9460,9462,9464,9466],{"class":203,"line":232},[201,9451,1087],{"class":214},[201,9453,6800],{"class":214},[201,9455,979],{"class":228},[201,9457,1265],{"class":965},[201,9459,1177],{"class":214},[201,9461,1199],{"class":291},[201,9463,847],{"class":228},[201,9465,1274],{"class":291},[201,9467,1003],{"class":228},[201,9469,9470,9473,9475,9477,9480,9482,9485],{"class":203,"line":245},[201,9471,9472],{"class":228},"        authHeader ",[201,9474,1214],{"class":214},[201,9476,1331],{"class":228},[201,9478,9479],{"class":291},"GetHeader",[201,9481,979],{"class":228},[201,9483,9484],{"class":238},"\"Authorization\"",[201,9486,822],{"class":228},[201,9488,9489,9491,9494,9496,9498],{"class":203,"line":256},[201,9490,1346],{"class":214},[201,9492,9493],{"class":228}," authHeader ",[201,9495,1015],{"class":214},[201,9497,2810],{"class":238},[201,9499,229],{"class":228},[201,9501,9502,9504,9507,9509,9512,9514,9516,9518,9520,9522,9525,9527,9530],{"class":203,"line":267},[201,9503,1375],{"class":228},[201,9505,9506],{"class":291},"AbortWithStatusJSON",[201,9508,979],{"class":228},[201,9510,9511],{"class":221},"401",[201,9513,338],{"class":228},[201,9515,1199],{"class":291},[201,9517,847],{"class":228},[201,9519,9259],{"class":291},[201,9521,2605],{"class":228},[201,9523,9524],{"class":238},"\"message\"",[201,9526,301],{"class":228},[201,9528,9529],{"class":238},"\"缺少认证令牌\"",[201,9531,627],{"class":228},[201,9533,9534],{"class":203,"line":273},[201,9535,9536],{"class":214},"            return\n",[201,9538,9539],{"class":203,"line":819},[201,9540,1440],{"class":228},[201,9542,9543],{"class":203,"line":825},[201,9544,779],{"emptyLinePlaceholder":394},[201,9546,9547,9550,9552,9554,9557,9560,9563,9565,9567],{"class":203,"line":830},[201,9548,9549],{"class":228},"        parts ",[201,9551,1214],{"class":214},[201,9553,1064],{"class":228},[201,9555,9556],{"class":291},"SplitN",[201,9558,9559],{"class":228},"(authHeader, ",[201,9561,9562],{"class":238},"\" \"",[201,9564,338],{"class":228},[201,9566,8008],{"class":221},[201,9568,822],{"class":228},[201,9570,9571,9573,9576,9579,9581,9583,9585,9588,9591,9594,9597,9599,9602,9605],{"class":203,"line":836},[201,9572,1346],{"class":214},[201,9574,9575],{"class":291}," len",[201,9577,9578],{"class":228},"(parts) ",[201,9580,3793],{"class":214},[201,9582,2557],{"class":221},[201,9584,1356],{"class":214},[201,9586,9587],{"class":214}," !",[201,9589,9590],{"class":228},"strings.",[201,9592,9593],{"class":291},"EqualFold",[201,9595,9596],{"class":228},"(parts[",[201,9598,1655],{"class":221},[201,9600,9601],{"class":228},"], ",[201,9603,9604],{"class":238},"\"Bearer\"",[201,9606,1003],{"class":228},[201,9608,9609,9611,9613,9615,9617,9619,9621,9623,9625,9627,9629,9631,9634],{"class":203,"line":853},[201,9610,1375],{"class":228},[201,9612,9506],{"class":291},[201,9614,979],{"class":228},[201,9616,9511],{"class":221},[201,9618,338],{"class":228},[201,9620,1199],{"class":291},[201,9622,847],{"class":228},[201,9624,9259],{"class":291},[201,9626,2605],{"class":228},[201,9628,9524],{"class":238},[201,9630,301],{"class":228},[201,9632,9633],{"class":238},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[201,9635,627],{"class":228},[201,9637,9638],{"class":203,"line":858},[201,9639,9536],{"class":214},[201,9641,9642],{"class":203,"line":864},[201,9643,1440],{"class":228},[201,9645,9646],{"class":203,"line":903},[201,9647,779],{"emptyLinePlaceholder":394},[201,9649,9650,9653,9655,9658,9661,9663,9665],{"class":203,"line":908},[201,9651,9652],{"class":228},"        user, err ",[201,9654,1214],{"class":214},[201,9656,9657],{"class":228}," authService.",[201,9659,9660],{"class":291},"GetUserByToken",[201,9662,9596],{"class":228},[201,9664,3299],{"class":221},[201,9666,9667],{"class":228},"])\n",[201,9669,9670,9672,9674,9676,9678],{"class":203,"line":922},[201,9671,1346],{"class":214},[201,9673,3790],{"class":228},[201,9675,3793],{"class":214},[201,9677,3796],{"class":221},[201,9679,229],{"class":228},[201,9681,9682,9684,9686,9688,9690,9692,9694,9696,9698,9700,9702,9704,9707],{"class":203,"line":931},[201,9683,1375],{"class":228},[201,9685,9506],{"class":291},[201,9687,979],{"class":228},[201,9689,9511],{"class":221},[201,9691,338],{"class":228},[201,9693,1199],{"class":291},[201,9695,847],{"class":228},[201,9697,9259],{"class":291},[201,9699,2605],{"class":228},[201,9701,9524],{"class":238},[201,9703,301],{"class":228},[201,9705,9706],{"class":238},"\"无效的认证令牌\"",[201,9708,627],{"class":228},[201,9710,9711],{"class":203,"line":946},[201,9712,9536],{"class":214},[201,9714,9715],{"class":203,"line":951},[201,9716,1440],{"class":228},[201,9718,9719],{"class":203,"line":956},[201,9720,779],{"emptyLinePlaceholder":394},[201,9722,9723,9725,9728,9730,9733],{"class":203,"line":1006},[201,9724,6929],{"class":228},[201,9726,9727],{"class":291},"Set",[201,9729,979],{"class":228},[201,9731,9732],{"class":238},"\"userID\"",[201,9734,9735],{"class":228},", user.ID)\n",[201,9737,9738,9740,9742,9744,9747],{"class":203,"line":1023},[201,9739,6929],{"class":228},[201,9741,9727],{"class":291},[201,9743,979],{"class":228},[201,9745,9746],{"class":238},"\"username\"",[201,9748,9749],{"class":228},", user.Username)\n",[201,9751,9752,9754,9757],{"class":203,"line":1035},[201,9753,6929],{"class":228},[201,9755,9756],{"class":291},"Next",[201,9758,1223],{"class":228},[201,9760,9761],{"class":203,"line":1046},[201,9762,1081],{"class":228},[201,9764,9765],{"class":203,"line":1078},[201,9766,276],{"class":228},[191,9768,9770],{"className":757,"code":9769,"language":759,"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,9771,9772,9777,9786,9792,9805,9817,9821,9825,9842,9862,9876,9900,9922,9937,9945,9949],{"__ignoreMap":196},[201,9773,9774],{"class":203,"line":204},[201,9775,9776],{"class":207},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[201,9778,9779,9781,9784],{"class":203,"line":211},[201,9780,911],{"class":214},[201,9782,9783],{"class":291}," ctxKey",[201,9785,2381],{"class":214},[201,9787,9788,9790],{"class":203,"line":232},[201,9789,2394],{"class":214},[201,9791,787],{"class":228},[201,9793,9794,9797,9800,9802],{"class":203,"line":245},[201,9795,9796],{"class":221},"    ctxUserID",[201,9798,9799],{"class":291},"   ctxKey",[201,9801,225],{"class":214},[201,9803,9804],{"class":238}," \"userID\"\n",[201,9806,9807,9810,9812,9814],{"class":203,"line":256},[201,9808,9809],{"class":221},"    ctxUsername",[201,9811,9783],{"class":291},[201,9813,225],{"class":214},[201,9815,9816],{"class":238}," \"username\"\n",[201,9818,9819],{"class":203,"line":267},[201,9820,822],{"class":228},[201,9822,9823],{"class":203,"line":273},[201,9824,779],{"emptyLinePlaceholder":394},[201,9826,9827,9829,9832,9834,9836,9838,9840],{"class":203,"line":819},[201,9828,959],{"class":214},[201,9830,9831],{"class":291}," WithUserContext",[201,9833,4073],{"class":228},[201,9835,1199],{"class":291},[201,9837,847],{"class":228},[201,9839,6767],{"class":291},[201,9841,229],{"class":228},[201,9843,9844,9846,9848,9850,9852,9854,9856,9858,9860],{"class":203,"line":825},[201,9845,1087],{"class":214},[201,9847,6800],{"class":214},[201,9849,979],{"class":228},[201,9851,1265],{"class":965},[201,9853,1177],{"class":214},[201,9855,1199],{"class":291},[201,9857,847],{"class":228},[201,9859,1274],{"class":291},[201,9861,1003],{"class":228},[201,9863,9864,9867,9869,9872,9874],{"class":203,"line":830},[201,9865,9866],{"class":228},"        ctx ",[201,9868,1214],{"class":214},[201,9870,9871],{"class":228}," c.Request.",[201,9873,1274],{"class":291},[201,9875,1223],{"class":228},[201,9877,9878,9880,9882,9884,9887,9890,9893,9895,9897],{"class":203,"line":836},[201,9879,9866],{"class":228},[201,9881,1029],{"class":214},[201,9883,3706],{"class":228},[201,9885,9886],{"class":291},"WithValue",[201,9888,9889],{"class":228},"(ctx, ctxUserID, c.",[201,9891,9892],{"class":291},"GetUint",[201,9894,979],{"class":228},[201,9896,9732],{"class":238},[201,9898,9899],{"class":228},"))\n",[201,9901,9902,9904,9906,9908,9910,9913,9916,9918,9920],{"class":203,"line":853},[201,9903,9866],{"class":228},[201,9905,1029],{"class":214},[201,9907,3706],{"class":228},[201,9909,9886],{"class":291},[201,9911,9912],{"class":228},"(ctx, ctxUsername, c.",[201,9914,9915],{"class":291},"GetString",[201,9917,979],{"class":228},[201,9919,9746],{"class":238},[201,9921,9899],{"class":228},[201,9923,9924,9927,9929,9931,9934],{"class":203,"line":858},[201,9925,9926],{"class":228},"        c.Request ",[201,9928,1029],{"class":214},[201,9930,9871],{"class":228},[201,9932,9933],{"class":291},"WithContext",[201,9935,9936],{"class":228},"(ctx)\n",[201,9938,9939,9941,9943],{"class":203,"line":864},[201,9940,6929],{"class":228},[201,9942,9756],{"class":291},[201,9944,1223],{"class":228},[201,9946,9947],{"class":203,"line":903},[201,9948,1081],{"class":228},[201,9950,9951],{"class":203,"line":908},[201,9952,276],{"class":228},[564,9954,9955],{"id":9955},"组合使用",[191,9957,9959],{"className":757,"code":9958,"language":759,"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,9960,9961,9973,9984,9995,10008],{"__ignoreMap":196},[201,9962,9963,9965,9967,9969,9971],{"class":203,"line":204},[201,9964,9006],{"class":228},[201,9966,9009],{"class":291},[201,9968,979],{"class":228},[201,9970,8969],{"class":238},[201,9972,242],{"class":228},[201,9974,9975,9978,9981],{"class":203,"line":211},[201,9976,9977],{"class":291},"    AuthRequired",[201,9979,9980],{"class":228},"(authService),  ",[201,9982,9983],{"class":207},"\u002F\u002F 1. 验证 Token\n",[201,9985,9986,9989,9992],{"class":203,"line":232},[201,9987,9988],{"class":291},"    WithUserContext",[201,9990,9991],{"class":228},"(),           ",[201,9993,9994],{"class":207},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[201,9996,9997,10000,10002,10005],{"class":203,"line":245},[201,9998,9999],{"class":228},"    gin.",[201,10001,9019],{"class":291},[201,10003,10004],{"class":228},"(mcpHTTP),         ",[201,10006,10007],{"class":207},"\u002F\u002F 3. MCP 处理\n",[201,10009,10010],{"class":203,"line":256},[201,10011,822],{"class":228},[17,10013,10014],{},"在工具处理器里就能拿到用户信息了：",[191,10016,10018],{"className":757,"code":10017,"language":759,"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,10019,10020,10075,10096,10105,10122,10126,10130,10146,10151,10164],{"__ignoreMap":196},[201,10021,10022,10024,10026,10029,10031,10034,10036,10039,10041,10043,10045,10047,10049,10051,10053,10055,10057,10059,10061,10063,10065,10067,10069,10071,10073],{"class":203,"line":204},[201,10023,959],{"class":214},[201,10025,962],{"class":228},[201,10027,10028],{"class":965},"h ",[201,10030,937],{"class":214},[201,10032,10033],{"class":291},"Handler",[201,10035,973],{"class":228},[201,10037,10038],{"class":291},"handleListItems",[201,10040,979],{"class":228},[201,10042,8626],{"class":965},[201,10044,8629],{"class":291},[201,10046,847],{"class":228},[201,10048,1274],{"class":291},[201,10050,338],{"class":228},[201,10052,8638],{"class":965},[201,10054,5259],{"class":291},[201,10056,847],{"class":228},[201,10058,8645],{"class":291},[201,10060,988],{"class":228},[201,10062,937],{"class":214},[201,10064,8652],{"class":291},[201,10066,847],{"class":228},[201,10068,8657],{"class":291},[201,10070,338],{"class":228},[201,10072,1000],{"class":214},[201,10074,1003],{"class":228},[201,10076,10077,10080,10082,10085,10088,10091,10094],{"class":203,"line":211},[201,10078,10079],{"class":228},"    userID, ok ",[201,10081,1214],{"class":214},[201,10083,10084],{"class":228}," ctx.",[201,10086,10087],{"class":291},"Value",[201,10089,10090],{"class":228},"(ctxUserID).(",[201,10092,10093],{"class":214},"uint",[201,10095,822],{"class":228},[201,10097,10098,10100,10102],{"class":203,"line":232},[201,10099,1009],{"class":214},[201,10101,9587],{"class":214},[201,10103,10104],{"class":228},"ok {\n",[201,10106,10107,10109,10111,10113,10115,10118,10120],{"class":203,"line":245},[201,10108,4117],{"class":214},[201,10110,8570],{"class":228},[201,10112,8869],{"class":291},[201,10114,979],{"class":228},[201,10116,10117],{"class":238},"\"未认证\"",[201,10119,8685],{"class":228},[201,10121,8688],{"class":221},[201,10123,10124],{"class":203,"line":256},[201,10125,1081],{"class":228},[201,10127,10128],{"class":203,"line":267},[201,10129,779],{"emptyLinePlaceholder":394},[201,10131,10132,10135,10137,10140,10143],{"class":203,"line":273},[201,10133,10134],{"class":228},"    items, _ ",[201,10136,1214],{"class":214},[201,10138,10139],{"class":228}," h.service.",[201,10141,10142],{"class":291},"ListByUser",[201,10144,10145],{"class":228},"(userID)\n",[201,10147,10148],{"class":203,"line":819},[201,10149,10150],{"class":207},"    \u002F\u002F ...\n",[201,10152,10153,10155,10157,10159,10162],{"class":203,"line":825},[201,10154,1087],{"class":214},[201,10156,8570],{"class":228},[201,10158,8677],{"class":291},[201,10160,10161],{"class":228},"(result), ",[201,10163,8688],{"class":221},[201,10165,10166],{"class":203,"line":830},[201,10167,276],{"class":228},[564,10169,10170],{"id":10170},"客户端配置",[17,10172,10173],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[191,10175,10177],{"className":719,"code":10176,"language":721,"meta":196,"style":196},"{\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",[198,10178,10179,10183,10189,10195,10207,10214,10224,10229,10233,10237],{"__ignoreMap":196},[201,10180,10181],{"class":203,"line":204},[201,10182,5732],{"class":228},[201,10184,10185,10187],{"class":203,"line":211},[201,10186,5737],{"class":221},[201,10188,736],{"class":228},[201,10190,10191,10193],{"class":203,"line":232},[201,10192,8462],{"class":221},[201,10194,736],{"class":228},[201,10196,10197,10200,10202,10205],{"class":203,"line":245},[201,10198,10199],{"class":221},"      \"url\"",[201,10201,301],{"class":228},[201,10203,10204],{"class":238},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[201,10206,242],{"class":228},[201,10208,10209,10212],{"class":203,"line":256},[201,10210,10211],{"class":221},"      \"headers\"",[201,10213,736],{"class":228},[201,10215,10216,10219,10221],{"class":203,"line":267},[201,10217,10218],{"class":221},"        \"Authorization\"",[201,10220,301],{"class":228},[201,10222,10223],{"class":238},"\"Bearer your-token-here\"\n",[201,10225,10226],{"class":203,"line":273},[201,10227,10228],{"class":228},"      }\n",[201,10230,10231],{"class":203,"line":819},[201,10232,1081],{"class":228},[201,10234,10235],{"class":203,"line":825},[201,10236,5784],{"class":228},[201,10238,10239],{"class":203,"line":830},[201,10240,276],{"class":228},[12,10242,2054],{"id":2054},[35,10244,10245,10254],{},[38,10246,10247],{},[41,10248,10249,10252],{},[44,10250,10251],{},"步骤",[44,10253,2185],{},[51,10255,10256,10265,10276,10285,10294],{},[41,10257,10258,10262],{},[56,10259,10260],{},[198,10261,8454],{},[56,10263,10264],{},"创建 MCP 协议实例",[41,10266,10267,10273],{},[56,10268,10269,4652,10271],{},[198,10270,8573],{},[198,10272,8616],{},[56,10274,10275],{},"定义并注册工具",[41,10277,10278,10282],{},[56,10279,10280],{},[198,10281,8954],{},[56,10283,10284],{},"转为 HTTP Handler",[41,10286,10287,10291],{},[56,10288,10289],{},[198,10290,8922],{},[56,10292,10293],{},"挂载到 Gin 路由",[41,10295,10296,10304],{},[56,10297,10298,4652,10301],{},[198,10299,10300],{},"AuthRequired",[198,10302,10303],{},"WithUserContext",[56,10305,10306],{},"鉴权 + 用户信息桥接",[17,10308,10309,10310,10313,10314,1739],{},"核心就一句话：",[21,10311,10312],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[198,10315,9410],{},[438,10317,10318],{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}",{"title":196,"searchDepth":211,"depth":232,"links":10320},[10321,10322,10323,10324,10331,10337],{"id":8337,"depth":211,"text":8338},{"id":8347,"depth":211,"text":8348},{"id":8364,"depth":211,"text":8364},{"id":8390,"depth":211,"text":8390,"children":10325},[10326,10327,10328,10329,10330],{"id":8393,"depth":232,"text":8394},{"id":8420,"depth":232,"text":8421},{"id":8534,"depth":232,"text":8535},{"id":8911,"depth":232,"text":8912},{"id":9032,"depth":232,"text":9033},{"id":9383,"depth":211,"text":9384,"children":10332},[10333,10334,10335,10336],{"id":4710,"depth":232,"text":4710},{"id":9403,"depth":232,"text":9404},{"id":9955,"depth":232,"text":9955},{"id":10170,"depth":232,"text":10170},{"id":2054,"depth":211,"text":2054},"介绍如何使用 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":8332,"description":10338},"posts\u002Fgo-mcp-gin\u002Findex",[2126,6594,4843,6595,4933],"2026-06-01","5fZl1Vigs2lHb5fHPz-0YZFyFD38ws1heOouZUG1g6M",{"id":10349,"title":10350,"author":23,"body":10351,"description":10616,"difficulty":2117,"draft":452,"extension":453,"image":10617,"meta":10618,"navigation":394,"path":10619,"pinned":452,"published":10620,"seo":10621,"series":459,"seriesOrder":232,"stem":10622,"tags":10623,"updated":464,"__hash__":10627},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题",{"type":9,"value":10352,"toc":10609},[10353,10356,10359,10374,10377,10380,10383,10424,10435,10441,10463,10470,10474,10477,10480,10483,10486,10509,10520,10577,10582,10595,10597,10603,10606],[12,10354,10355],{"id":10355},"问题描述",[17,10357,10358],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[121,10360,10361,10371],{},[124,10362,10363,10364,10367,10368],{},"首次通过客户端导航（点击 ",[198,10365,10366],{},"\u003CNuxtLink>","）跳转到某个页面时，",[21,10369,10370],{},"所有链接文字都会出现下划线",[124,10372,10373],{},"手动刷新页面后，下划线消失，样式恢复正常",[17,10375,10376],{},"这个问题在开发模式和生产构建后都会出现。",[12,10378,10379],{"id":10379},"原因分析",[17,10381,10382],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[191,10384,10388],{"className":10385,"code":10386,"language":10387,"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,10389,10390,10396,10409,10420],{"__ignoreMap":196},[201,10391,10392,10394],{"class":203,"line":204},[201,10393,422],{"class":297},[201,10395,229],{"class":228},[201,10397,10398,10401,10403,10406],{"class":203,"line":211},[201,10399,10400],{"class":221},"  color",[201,10402,301],{"class":228},[201,10404,10405],{"class":221},"inherit",[201,10407,10408],{"class":228},";\n",[201,10410,10411,10414,10416,10418],{"class":203,"line":232},[201,10412,10413],{"class":221},"  text-decoration",[201,10415,301],{"class":228},[201,10417,10405],{"class":221},[201,10419,10408],{"class":228},[201,10421,10422],{"class":203,"line":245},[201,10423,276],{"class":228},[17,10425,10426,10427,10430,10431,10434],{},"正常情况下，这会让链接继承父元素的 ",[198,10428,10429],{},"text-decoration","（通常是 ",[198,10432,10433],{},"none","），从而去掉浏览器默认的下划线。",[17,10436,10437,10438,578],{},"问题出在 ",[21,10439,10440],{},"Nuxt 的客户端导航机制",[509,10442,10443,10449,10452,10457,10460],{},[124,10444,10445,10446,10448],{},"用户点击 ",[198,10447,10366],{},"，Vue Router 拦截导航",[124,10450,10451],{},"新页面组件异步加载并渲染",[124,10453,10454],{},[21,10455,10456],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[124,10458,10459],{},"此时浏览器使用默认样式（链接带下划线）",[124,10461,10462],{},"CSS 加载完成后，下划线消失",[17,10464,10465,10466,10469],{},"这就是典型的 ",[21,10467,10468],{},"FOUC（Flash of Unstyled Content）"," 问题。",[12,10471,10473],{"id":10472},"为什么刷新就正常了","为什么刷新就正常了？",[17,10475,10476],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[17,10478,10479],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[12,10481,10482],{"id":10482},"解决方案",[17,10484,10485],{},"尝试了多种方案：",[121,10487,10488,10494,10500,10506],{},[124,10489,10490,10493],{},[198,10491,10492],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[124,10495,10496,10499],{},[198,10497,10498],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[124,10501,10502,10505],{},[198,10503,10504],{},"features.inlineStyles: true"," — 无效",[124,10507,10508],{},"页面过渡动画 — 只是掩盖，不能根治",[17,10510,10511,10512,10515,10516,10519],{},"最终有效的方案是在 ",[198,10513,10514],{},"app.vue"," 中用 ",[198,10517,10518],{},"!important"," 强制覆盖：",[191,10521,10525],{"className":10522,"code":10523,"language":10524,"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,10526,10527,10535,10551,10565,10569],{"__ignoreMap":196},[201,10528,10529,10531,10533],{"class":203,"line":204},[201,10530,644],{"class":228},[201,10532,438],{"class":297},[201,10534,649],{"class":228},[201,10536,10537,10539,10542,10544,10547,10549],{"class":203,"line":211},[201,10538,422],{"class":297},[201,10540,10541],{"class":291},":not",[201,10543,979],{"class":228},[201,10545,10546],{"class":291},".prose",[201,10548,3265],{"class":297},[201,10550,1003],{"class":228},[201,10552,10553,10556,10558,10560,10563],{"class":203,"line":232},[201,10554,10555],{"class":221},"  text-decoration-line",[201,10557,301],{"class":228},[201,10559,10433],{"class":221},[201,10561,10562],{"class":214}," !important",[201,10564,10408],{"class":228},[201,10566,10567],{"class":203,"line":245},[201,10568,276],{"class":228},[201,10570,10571,10573,10575],{"class":203,"line":256},[201,10572,669],{"class":228},[201,10574,438],{"class":297},[201,10576,649],{"class":228},[17,10578,10579],{},[21,10580,10581],{},"关键点：",[121,10583,10584,10589],{},[124,10585,10586,10588],{},[198,10587,10518],{}," 是为了对抗 Tailwind CSS layers 的优先级",[124,10590,10591,10594],{},[198,10592,10593],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[12,10596,2054],{"id":2054},[17,10598,10599,10600,10602],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[198,10601,10518],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[17,10604,10605],{},"如果你有更好的方案，欢迎在项目仓库的 Issues 中提出！",[438,10607,10608],{},"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":10610},[10611,10612,10613,10614,10615],{"id":10355,"depth":211,"text":10355},{"id":10379,"depth":211,"text":10379},{"id":10472,"depth":211,"text":10473},{"id":10482,"depth":211,"text":10482},{"id":2054,"depth":211,"text":2054},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":10350,"description":10616},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[463,10624,10625,10626],"Tailwind CSS","CSS","踩坑","WMLpTEsIoJqufwEfXyIiBgUYxcPY2XkvOwqAD4hbe6g",{"id":10629,"title":10630,"author":7,"body":10631,"description":11762,"difficulty":2117,"draft":452,"extension":453,"image":11763,"meta":11764,"navigation":394,"path":11765,"pinned":452,"published":11766,"seo":11767,"series":459,"seriesOrder":211,"stem":11768,"tags":11769,"updated":464,"__hash__":11771},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",{"type":9,"value":10632,"toc":11752},[10633,10636,10639,10642,10646,10649,10659,10662,10669,10733,10740,10743,10750,11009,11019,11023,11026,11029,11054,11068,11071,11094,11100,11103,11106,11109,11114,11121,11336,11341,11385,11389,11392,11399,11402,11449,11455,11680,11690,11692,11743,11746,11749],[10634,10635,473],"h1",{"id":473},[17,10637,10638],{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[17,10640,10641],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[10634,10643,10645],{"id":10644},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[12,10647,10648],{"id":10648},"现象",[17,10650,10651,10652,10655,10656,1739],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[198,10653,10654],{},"post.body.toc"," 上。直接用它渲染目录，发现",[21,10657,10658],{},"只有 h2 标题，h3 全部丢失",[12,10660,10661],{"id":10661},"排查",[17,10663,10664,10665,10668],{},"查了官方文档，发现 ",[198,10666,10667],{},"build.markdown.toc.depth"," 可以控制深度：",[191,10670,10672],{"className":193,"code":10671,"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,10673,10674,10679,10686,10693,10700,10721,10725,10729],{"__ignoreMap":196},[201,10675,10676],{"class":203,"line":204},[201,10677,10678],{"class":207},"\u002F\u002F nuxt.config.ts\n",[201,10680,10681,10684],{"class":203,"line":211},[201,10682,10683],{"class":291},"content",[201,10685,736],{"class":228},[201,10687,10688,10691],{"class":203,"line":232},[201,10689,10690],{"class":291},"  build",[201,10692,736],{"class":228},[201,10694,10695,10698],{"class":203,"line":245},[201,10696,10697],{"class":291},"    markdown",[201,10699,736],{"class":228},[201,10701,10702,10705,10708,10711,10713,10715,10718],{"class":203,"line":256},[201,10703,10704],{"class":291},"      toc",[201,10706,10707],{"class":228},": { ",[201,10709,10710],{"class":291},"depth",[201,10712,301],{"class":228},[201,10714,3721],{"class":221},[201,10716,10717],{"class":228}," }  ",[201,10719,10720],{"class":207},"\u002F\u002F 包含 h3\n",[201,10722,10723],{"class":203,"line":267},[201,10724,1081],{"class":228},[201,10726,10727],{"class":203,"line":273},[201,10728,5784],{"class":228},[201,10730,10731],{"class":203,"line":819},[201,10732,276],{"class":228},[17,10734,10735,10736,10739],{},"配置改了，清了缓存重启，",[21,10737,10738],{},"依然只有 h2","。这个配置似乎不起作用。",[12,10741,10742],{"id":10742},"最终方案",[17,10744,10745,10746,10749],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[198,10747,10748],{},"post.body"," 的 AST 里递归提取所有标题：",[191,10751,10753],{"className":193,"code":10752,"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,10754,10755,10797,10814,10827,10832,10885,10912,10942,10966,10970,10994,10998,11005],{"__ignoreMap":196},[201,10756,10757,10760,10763,10765,10768,10770,10773,10775,10778,10780,10783,10785,10787,10790,10792,10794],{"class":203,"line":204},[201,10758,10759],{"class":214},"function",[201,10761,10762],{"class":291}," extractHeadings",[201,10764,979],{"class":228},[201,10766,10767],{"class":965},"node",[201,10769,2400],{"class":214},[201,10771,10772],{"class":221}," any",[201,10774,338],{"class":228},[201,10776,10777],{"class":965},"list",[201,10779,2400],{"class":214},[201,10781,10782],{"class":291}," Heading",[201,10784,2405],{"class":228},[201,10786,1029],{"class":214},[201,10788,10789],{"class":228}," [])",[201,10791,2400],{"class":214},[201,10793,10782],{"class":291},[201,10795,10796],{"class":228},"[] {\n",[201,10798,10799,10802,10804,10807,10810,10812],{"class":203,"line":211},[201,10800,10801],{"class":214},"  if",[201,10803,962],{"class":228},[201,10805,10806],{"class":214},"!",[201,10808,10809],{"class":228},"node) ",[201,10811,3314],{"class":214},[201,10813,6164],{"class":228},[201,10815,10816,10818,10821,10824],{"class":203,"line":232},[201,10817,10801],{"class":214},[201,10819,10820],{"class":228}," (Array.",[201,10822,10823],{"class":291},"isArray",[201,10825,10826],{"class":228},"(node)) {\n",[201,10828,10829],{"class":203,"line":245},[201,10830,10831],{"class":207},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[201,10833,10834,10836,10838,10841,10844,10846,10849,10851,10854,10857,10860,10862,10865,10868,10870,10872,10874,10877,10880,10882],{"class":203,"line":256},[201,10835,1009],{"class":214},[201,10837,962],{"class":228},[201,10839,10840],{"class":214},"typeof",[201,10842,10843],{"class":228}," node[",[201,10845,1655],{"class":221},[201,10847,10848],{"class":228},"] ",[201,10850,7437],{"class":214},[201,10852,10853],{"class":238}," 'string'",[201,10855,10856],{"class":214}," &&",[201,10858,10859],{"class":238}," \u002F",[201,10861,7356],{"class":214},[201,10863,10864],{"class":7359},"h",[201,10866,10867],{"class":221},"[1-6]",[201,10869,2023],{"class":214},[201,10871,7353],{"class":238},[201,10873,847],{"class":228},[201,10875,10876],{"class":291},"test",[201,10878,10879],{"class":228},"(node[",[201,10881,1655],{"class":221},[201,10883,10884],{"class":228},"])) {\n",[201,10886,10887,10889,10892,10894,10897,10899,10901,10903,10906,10908,10910],{"class":203,"line":267},[201,10888,2694],{"class":214},[201,10890,10891],{"class":221}," level",[201,10893,225],{"class":214},[201,10895,10896],{"class":291}," Number",[201,10898,10879],{"class":228},[201,10900,1655],{"class":221},[201,10902,3275],{"class":228},[201,10904,10905],{"class":291},"charAt",[201,10907,979],{"class":228},[201,10909,3299],{"class":221},[201,10911,9899],{"class":228},[201,10913,10914,10916,10919,10921,10924,10927,10930,10932,10934,10937,10940],{"class":203,"line":273},[201,10915,2694],{"class":214},[201,10917,10918],{"class":221}," text",[201,10920,225],{"class":214},[201,10922,10923],{"class":291}," extractText",[201,10925,10926],{"class":228},"(node.",[201,10928,10929],{"class":291},"slice",[201,10931,979],{"class":228},[201,10933,8008],{"class":221},[201,10935,10936],{"class":228},")).",[201,10938,10939],{"class":291},"trim",[201,10941,1223],{"class":228},[201,10943,10944,10947,10950,10953,10955,10958,10960,10963],{"class":203,"line":819},[201,10945,10946],{"class":228},"      list.",[201,10948,10949],{"class":291},"push",[201,10951,10952],{"class":228},"({ id: node[",[201,10954,3299],{"class":221},[201,10956,10957],{"class":228},"]?.id ",[201,10959,2452],{"class":214},[201,10961,10962],{"class":291}," slugify",[201,10964,10965],{"class":228},"(text), text, level })\n",[201,10967,10968],{"class":203,"line":825},[201,10969,1081],{"class":228},[201,10971,10972,10975,10977,10979,10982,10985,10988,10991],{"class":203,"line":830},[201,10973,10974],{"class":214},"    for",[201,10976,962],{"class":228},[201,10978,2394],{"class":214},[201,10980,10981],{"class":221}," child",[201,10983,10984],{"class":214}," of",[201,10986,10987],{"class":228}," node) ",[201,10989,10990],{"class":291},"extractHeadings",[201,10992,10993],{"class":228},"(child, list)\n",[201,10995,10996],{"class":203,"line":836},[201,10997,5784],{"class":228},[201,10999,11000,11003],{"class":203,"line":853},[201,11001,11002],{"class":214},"  return",[201,11004,6164],{"class":228},[201,11006,11007],{"class":203,"line":858},[201,11008,276],{"class":228},[17,11010,11011,11012,11014,11015,11018],{},"关键点：Nuxt Content v3 的 body 使用 ",[21,11013,9],{}," 格式，标题节点是数组 ",[198,11016,11017],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[10634,11020,11022],{"id":11021},"问题二图片路径解析错误","问题二：图片路径解析错误",[12,11024,10648],{"id":11025},"现象-1",[17,11027,11028],{},"markdown 里用相对路径引用图片：",[191,11030,11034],{"className":11031,"code":11032,"language":11033,"meta":196,"style":196},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[198,11035,11036],{"__ignoreMap":196},[201,11037,11038,11041,11045,11048,11052],{"class":203,"line":204},[201,11039,11040],{"class":228},"![",[201,11042,11044],{"class":11043},"smiIW","SSHFS挂载效果",[201,11046,11047],{"class":228},"](",[201,11049,11051],{"class":11050},"szbl0","img\u002Fsshfs.avif",[201,11053,822],{"class":228},[17,11055,11056,11057,11060,11061,11064,11065,1739],{},"HTML 输出的 ",[198,11058,11059],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[198,11062,11063],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[198,11066,11067],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[12,11069,10661],{"id":11070},"排查-1",[509,11072,11073,11080,11083,11089],{},[124,11074,11075,11076,11079],{},"检查了 ",[198,11077,11078],{},"\u003Cbase>"," 标签 —— 没有",[124,11081,11082],{},"检查了重定向 —— 没有",[124,11084,11085,11086,11088],{},"直接访问 ",[198,11087,11067],{}," —— 返回 200 OK",[124,11090,11085,11091,11093],{},[198,11092,11063],{}," —— 404",[17,11095,11096,11097,1739],{},"服务器路由没问题，问题是",[21,11098,11099],{},"浏览器解析相对路径时基于了错误的基准 URL",[17,11101,11102],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[12,11104,10742],{"id":11105},"最终方案-1",[17,11107,11108],{},"两步解决：",[17,11110,11111],{},[21,11112,11113],{},"第一步：自定义 ProseImg 组件",[17,11115,11116,11117,11120],{},"创建 ",[198,11118,11119],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[191,11122,11124],{"className":10522,"code":11123,"language":10524,"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,11125,11126,11138,11152,11171,11187,11224,11256,11278,11282,11290,11294,11303,11328],{"__ignoreMap":196},[201,11127,11128,11130,11133,11136],{"class":203,"line":204},[201,11129,644],{"class":228},[201,11131,11132],{"class":297},"script",[201,11134,11135],{"class":291}," setup",[201,11137,649],{"class":228},[201,11139,11140,11142,11145,11147,11150],{"class":203,"line":211},[201,11141,2394],{"class":214},[201,11143,11144],{"class":221}," route",[201,11146,225],{"class":214},[201,11148,11149],{"class":291}," useRoute",[201,11151,1223],{"class":228},[201,11153,11154,11156,11159,11161,11164,11167,11169],{"class":203,"line":232},[201,11155,2394],{"class":214},[201,11157,11158],{"class":221}," resolvedSrc",[201,11160,225],{"class":214},[201,11162,11163],{"class":291}," computed",[201,11165,11166],{"class":228},"(() ",[201,11168,2424],{"class":214},[201,11170,229],{"class":228},[201,11172,11173,11175,11177,11179,11182,11184],{"class":203,"line":245},[201,11174,10801],{"class":214},[201,11176,962],{"class":228},[201,11178,10806],{"class":214},[201,11180,11181],{"class":228},"props.src) ",[201,11183,3314],{"class":214},[201,11185,11186],{"class":238}," ''\n",[201,11188,11189,11191,11194,11197,11199,11202,11204,11206,11209,11211,11213,11216,11219,11221],{"class":203,"line":256},[201,11190,10801],{"class":214},[201,11192,11193],{"class":228}," (props.src.",[201,11195,11196],{"class":291},"startsWith",[201,11198,979],{"class":228},[201,11200,11201],{"class":238},"'\u002F'",[201,11203,973],{"class":228},[201,11205,2452],{"class":214},[201,11207,11208],{"class":228}," props.src.",[201,11210,11196],{"class":291},[201,11212,979],{"class":228},[201,11214,11215],{"class":238},"'http'",[201,11217,11218],{"class":228},")) ",[201,11220,3314],{"class":214},[201,11222,11223],{"class":228}," props.src\n",[201,11225,11226,11228,11231,11233,11236,11238,11240,11242,11246,11248,11250,11252,11254],{"class":203,"line":267},[201,11227,3162],{"class":214},[201,11229,11230],{"class":221}," base",[201,11232,225],{"class":214},[201,11234,11235],{"class":228}," route.path.",[201,11237,7348],{"class":291},[201,11239,979],{"class":228},[201,11241,7353],{"class":238},[201,11243,11245],{"class":11244},"sCnZR","\\\u002F",[201,11247,2023],{"class":214},[201,11249,7353],{"class":238},[201,11251,338],{"class":228},[201,11253,7366],{"class":238},[201,11255,822],{"class":228},[201,11257,11258,11260,11263,11266,11269,11271,11273,11275],{"class":203,"line":273},[201,11259,11002],{"class":214},[201,11261,11262],{"class":238}," `${",[201,11264,11265],{"class":228},"base",[201,11267,11268],{"class":238},"}\u002F${",[201,11270,2678],{"class":228},[201,11272,847],{"class":238},[201,11274,2307],{"class":228},[201,11276,11277],{"class":238},"}`\n",[201,11279,11280],{"class":203,"line":819},[201,11281,627],{"class":228},[201,11283,11284,11286,11288],{"class":203,"line":825},[201,11285,669],{"class":228},[201,11287,11132],{"class":297},[201,11289,649],{"class":228},[201,11291,11292],{"class":203,"line":830},[201,11293,779],{"emptyLinePlaceholder":394},[201,11295,11296,11298,11301],{"class":203,"line":836},[201,11297,644],{"class":228},[201,11299,11300],{"class":297},"template",[201,11302,649],{"class":228},[201,11304,11305,11307,11309,11312,11314,11317,11320,11322,11325],{"class":203,"line":853},[201,11306,654],{"class":228},[201,11308,2419],{"class":297},[201,11310,11311],{"class":291}," :src",[201,11313,1029],{"class":228},[201,11315,11316],{"class":238},"\"resolvedSrc\"",[201,11318,11319],{"class":291}," :alt",[201,11321,1029],{"class":228},[201,11323,11324],{"class":238},"\"alt\"",[201,11326,11327],{"class":228}," \u002F>\n",[201,11329,11330,11332,11334],{"class":203,"line":858},[201,11331,669],{"class":228},[201,11333,11300],{"class":297},[201,11335,649],{"class":228},[17,11337,11338],{},[21,11339,11340],{},"第二步：启用 MDC prose 组件",[191,11342,11344],{"className":193,"code":11343,"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,11345,11346,11350,11357,11364,11377,11381],{"__ignoreMap":196},[201,11347,11348],{"class":203,"line":204},[201,11349,10678],{"class":207},[201,11351,11352,11355],{"class":203,"line":211},[201,11353,11354],{"class":291},"mdc",[201,11356,736],{"class":228},[201,11358,11359,11362],{"class":203,"line":232},[201,11360,11361],{"class":291},"  components",[201,11363,736],{"class":228},[201,11365,11366,11369,11371,11374],{"class":203,"line":245},[201,11367,11368],{"class":291},"    prose",[201,11370,301],{"class":228},[201,11372,11373],{"class":221},"true",[201,11375,11376],{"class":207},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[201,11378,11379],{"class":203,"line":256},[201,11380,5784],{"class":228},[201,11382,11383],{"class":203,"line":267},[201,11384,276],{"class":228},[10634,11386,11388],{"id":11387},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[12,11390,10648],{"id":11391},"现象-2",[17,11393,11394,11395,11398],{},"原站（SvelteKit）的图片放在 ",[198,11396,11397],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[12,11400,11401],{"id":11401},"方案对比",[35,11403,11404,11414],{},[38,11405,11406],{},[41,11407,11408,11410,11412],{},[44,11409,2176],{},[44,11411,3644],{},[44,11413,3647],{},[51,11415,11416,11427,11438],{},[41,11417,11418,11421,11424],{},[56,11419,11420],{},"复制到 public\u002F",[56,11422,11423],{},"简单",[56,11425,11426],{},"文件重复，需同步",[41,11428,11429,11432,11435],{},[56,11430,11431],{},"Vite 插件拦截",[56,11433,11434],{},"原站方案",[56,11436,11437],{},"Nuxt 不直接支持",[41,11439,11440,11443,11446],{},[56,11441,11442],{},"Server Route",[56,11444,11445],{},"不复制文件",[56,11447,11448],{},"需要手动写路由",[17,11450,11451,11452,11454],{},"最终选择 ",[21,11453,11442],{},"，和原站的 Vite 插件思路一致：",[191,11456,11458],{"className":193,"code":11457,"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,11459,11460,11465,11490,11505,11566,11580,11584,11606,11632,11650,11669,11676],{"__ignoreMap":196},[201,11461,11462],{"class":203,"line":204},[201,11463,11464],{"class":207},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[201,11466,11467,11469,11471,11474,11476,11479,11481,11484,11486,11488],{"class":203,"line":211},[201,11468,215],{"class":214},[201,11470,597],{"class":214},[201,11472,11473],{"class":291}," defineEventHandler",[201,11475,979],{"class":228},[201,11477,11478],{"class":214},"async",[201,11480,962],{"class":228},[201,11482,11483],{"class":965},"event",[201,11485,973],{"class":228},[201,11487,2424],{"class":214},[201,11489,229],{"class":228},[201,11491,11492,11494,11497,11499,11502],{"class":203,"line":232},[201,11493,3162],{"class":214},[201,11495,11496],{"class":221}," url",[201,11498,225],{"class":214},[201,11500,11501],{"class":291}," getRequestURL",[201,11503,11504],{"class":228},"(event)\n",[201,11506,11507,11509,11512,11514,11517,11520,11522,11524,11526,11528,11531,11533,11535,11537,11539,11542,11544,11546,11548,11550,11552,11554,11556,11558,11560,11562,11564],{"class":203,"line":245},[201,11508,3162],{"class":214},[201,11510,11511],{"class":221}," match",[201,11513,225],{"class":214},[201,11515,11516],{"class":228}," url.pathname.",[201,11518,11519],{"class":291},"match",[201,11521,979],{"class":228},[201,11523,7353],{"class":238},[201,11525,7356],{"class":214},[201,11527,11245],{"class":11244},[201,11529,11530],{"class":7359},"posts",[201,11532,11245],{"class":11244},[201,11534,979],{"class":7359},[201,11536,4049],{"class":221},[201,11538,7356],{"class":214},[201,11540,11541],{"class":221},"\u002F]",[201,11543,1056],{"class":214},[201,11545,7369],{"class":7359},[201,11547,11245],{"class":11244},[201,11549,2419],{"class":7359},[201,11551,11245],{"class":11244},[201,11553,979],{"class":7359},[201,11555,847],{"class":221},[201,11557,1056],{"class":214},[201,11559,7369],{"class":7359},[201,11561,2023],{"class":214},[201,11563,7353],{"class":238},[201,11565,822],{"class":228},[201,11567,11568,11570,11572,11574,11577],{"class":203,"line":256},[201,11569,10801],{"class":214},[201,11571,962],{"class":228},[201,11573,10806],{"class":214},[201,11575,11576],{"class":228},"match) ",[201,11578,11579],{"class":214},"return\n",[201,11581,11582],{"class":203,"line":267},[201,11583,779],{"emptyLinePlaceholder":394},[201,11585,11586,11588,11591,11594,11596,11599,11601,11603],{"class":203,"line":273},[201,11587,3162],{"class":214},[201,11589,11590],{"class":228}," [, ",[201,11592,11593],{"class":221},"slug",[201,11595,338],{"class":228},[201,11597,11598],{"class":221},"filename",[201,11600,10848],{"class":228},[201,11602,1029],{"class":214},[201,11604,11605],{"class":228}," match\n",[201,11607,11608,11610,11613,11615,11618,11620,11623,11626,11629],{"class":203,"line":819},[201,11609,3162],{"class":214},[201,11611,11612],{"class":221}," filePath",[201,11614,225],{"class":214},[201,11616,11617],{"class":291}," resolve",[201,11619,979],{"class":228},[201,11621,11622],{"class":238},"'content\u002Fposts'",[201,11624,11625],{"class":228},", slug, ",[201,11627,11628],{"class":238},"'img'",[201,11630,11631],{"class":228},", filename)\n",[201,11633,11634,11636,11639,11641,11644,11647],{"class":203,"line":825},[201,11635,3162],{"class":214},[201,11637,11638],{"class":221}," data",[201,11640,225],{"class":214},[201,11642,11643],{"class":214}," await",[201,11645,11646],{"class":291}," readFile",[201,11648,11649],{"class":228},"(filePath)\n",[201,11651,11652,11655,11658,11661,11663,11666],{"class":203,"line":830},[201,11653,11654],{"class":291},"  setResponseHeader",[201,11656,11657],{"class":228},"(event, ",[201,11659,11660],{"class":238},"'content-type'",[201,11662,338],{"class":228},[201,11664,11665],{"class":221},"MIME",[201,11667,11668],{"class":228},"[ext])\n",[201,11670,11671,11673],{"class":203,"line":836},[201,11672,11002],{"class":214},[201,11674,11675],{"class":228}," data\n",[201,11677,11678],{"class":203,"line":853},[201,11679,627],{"class":228},[17,11681,11682,11683,11685,11686,11689],{},"请求 ",[198,11684,11067],{}," → 读取 ",[198,11687,11688],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[10634,11691,2054],{"id":2054},[35,11693,11694,11705],{},[38,11695,11696],{},[41,11697,11698,11700,11703],{},[44,11699,4710],{},[44,11701,11702],{},"根因",[44,11704,2176],{},[51,11706,11707,11721,11732],{},[41,11708,11709,11712,11718],{},[56,11710,11711],{},"TOC 只有 h2",[56,11713,11714,11717],{},[198,11715,11716],{},"toc.depth"," 配置不生效",[56,11719,11720],{},"从 body AST 手动提取",[41,11722,11723,11726,11729],{},[56,11724,11725],{},"图片路径错误",[56,11727,11728],{},"SPA 路由导致相对路径解析基准错误",[56,11730,11731],{},"ProseImg 组件 + mdc.prose 配置",[41,11733,11734,11737,11740],{},[56,11735,11736],{},"content 图片 404",[56,11738,11739],{},"content 目录不暴露给浏览器",[56,11741,11742],{},"Server Route 直接读取返回",[17,11744,11745],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[17,11747,11748],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[438,11750,11751],{},"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":11753},[11754,11755,11756,11757,11758,11759,11760,11761],{"id":10648,"depth":211,"text":10648},{"id":10661,"depth":211,"text":10661},{"id":10742,"depth":211,"text":10742},{"id":11025,"depth":211,"text":10648},{"id":11070,"depth":211,"text":10661},{"id":11105,"depth":211,"text":10742},{"id":11391,"depth":211,"text":10648},{"id":11401,"depth":211,"text":11401},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":10630,"description":11762},"posts\u002Fnuxt-content-lessons\u002Findex",[463,10626,11770],"Nuxt Content","vbSt6ND1z7xt7L8FxgZ8Qe3EdxV9InsRerN3E23GC7s",{"id":11773,"title":11774,"author":7,"body":11775,"description":11976,"difficulty":7,"draft":452,"extension":453,"image":11977,"meta":11978,"navigation":394,"path":11979,"pinned":452,"published":11980,"seo":11981,"series":7,"seriesOrder":7,"stem":11982,"tags":11983,"updated":7,"__hash__":11984},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":9,"value":11776,"toc":11966},[11777,11781,11784,11790,11793,11797,11804,11851,11855,11861,11867,11871,11874,11877,11944,11946,11957,11963],[12,11778,11780],{"id":11779},"什么是-sveltekit","什么是 SvelteKit？",[17,11782,11783],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[17,11785,11786],{},[2419,11787],{"alt":11788,"src":11789},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[12,11791,11792],{"id":11792},"核心特点",[564,11794,11796],{"id":11795},"_1-编译时框架","1. 编译时框架",[17,11798,11799,11800,11803],{},"Svelte 和 React\u002FVue 最大的区别在于：",[21,11801,11802],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[191,11805,11807],{"className":193,"code":11806,"language":195,"meta":196,"style":196},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n",[198,11808,11809,11814,11827,11836,11847],{"__ignoreMap":196},[201,11810,11811],{"class":203,"line":204},[201,11812,11813],{"class":207},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[201,11815,11816,11819,11822,11824],{"class":203,"line":211},[201,11817,11818],{"class":214},"let",[201,11820,11821],{"class":228}," count ",[201,11823,1029],{"class":214},[201,11825,11826],{"class":221}," 0\n",[201,11828,11829,11831,11834],{"class":203,"line":232},[201,11830,10759],{"class":214},[201,11832,11833],{"class":291}," increment",[201,11835,9048],{"class":228},[201,11837,11838,11841,11844],{"class":203,"line":245},[201,11839,11840],{"class":228},"  count ",[201,11842,11843],{"class":214},"+=",[201,11845,11846],{"class":221}," 1\n",[201,11848,11849],{"class":203,"line":256},[201,11850,276],{"class":228},[564,11852,11854],{"id":11853},"_2-文件路由","2. 文件路由",[17,11856,11857,11860],{},[198,11858,11859],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[191,11862,11865],{"className":11863,"code":11864,"language":3483},[3481],"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",[198,11866,11864],{"__ignoreMap":196},[564,11868,11870],{"id":11869},"_3-全栈能力","3. 全栈能力",[17,11872,11873],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[12,11875,11876],{"id":11876},"和其他框架对比",[35,11878,11879,11893],{},[38,11880,11881],{},[41,11882,11883,11886,11889,11891],{},[44,11884,11885],{},"特性",[44,11887,11888],{},"SvelteKit",[44,11890,4793],{},[44,11892,463],{},[51,11894,11895,11908,11921,11933],{},[41,11896,11897,11900,11903,11906],{},[56,11898,11899],{},"学习曲线",[56,11901,11902],{},"低",[56,11904,11905],{},"中",[56,11907,11905],{},[41,11909,11910,11913,11916,11919],{},[56,11911,11912],{},"包体积",[56,11914,11915],{},"小",[56,11917,11918],{},"大",[56,11920,11905],{},[41,11922,11923,11926,11928,11931],{},[56,11924,11925],{},"运行时开销",[56,11927,2199],{},[56,11929,11930],{},"有",[56,11932,11930],{},[41,11934,11935,11938,11940,11942],{},[56,11936,11937],{},"生态",[56,11939,11915],{},[56,11941,11918],{},[56,11943,11918],{},[12,11945,2054],{"id":2054},[17,11947,11948,11949,11952,11953,11956],{},"如果你追求",[21,11950,11951],{},"简洁的代码","和",[21,11954,11955],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[17,11958,11959],{},[2419,11960],{"alt":11961,"src":11962},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[438,11964,11965],{},"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":11967},[11968,11969,11974,11975],{"id":11779,"depth":211,"text":11780},{"id":11792,"depth":211,"text":11792,"children":11970},[11971,11972,11973],{"id":11795,"depth":232,"text":11796},{"id":11853,"depth":232,"text":11854},{"id":11869,"depth":232,"text":11870},{"id":11876,"depth":211,"text":11876},{"id":2054,"depth":211,"text":2054},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":11774,"description":11976},"posts\u002Fsveltekit-intro\u002Findex",[11888,4919],"vsEGyunpCWR7DiJAdyV1SPDfa57xXLITBI-lKM8UeGQ",{"id":11986,"title":11987,"author":7,"body":11988,"description":12391,"difficulty":451,"draft":452,"extension":453,"image":12392,"meta":12393,"navigation":394,"path":12394,"pinned":452,"published":12395,"seo":12396,"series":12397,"seriesOrder":211,"stem":12398,"tags":12399,"updated":12395,"__hash__":12403},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":9,"value":11989,"toc":12381},[11990,11993,11996,11998,12002,12005,12025,12029,12062,12066,12072,12120,12123,12341,12343,12346,12375,12378],[12,11991,11992],{"id":11992},"前言",[17,11994,11995],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[12,11997,10482],{"id":10482},[564,11999,12001],{"id":12000},"_1-安装用户管理工具","1. 安装用户管理工具",[17,12003,12004],{},"若镜像非自带用户管理：",[191,12006,12010],{"className":12007,"code":12008,"language":12009,"meta":196,"style":196},"language-dockerfile shiki shiki-themes github-light github-dark","# 权限管理\nRUN apk add --no-cache shadow\n","dockerfile",[198,12011,12012,12017],{"__ignoreMap":196},[201,12013,12014],{"class":203,"line":204},[201,12015,12016],{"class":207},"# 权限管理\n",[201,12018,12019,12022],{"class":203,"line":211},[201,12020,12021],{"class":214},"RUN",[201,12023,12024],{"class":228}," apk add --no-cache shadow\n",[564,12026,12028],{"id":12027},"_2-创建用户并修改权限","2. 创建用户并修改权限",[191,12030,12032],{"className":12007,"code":12031,"language":12009,"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,12033,12034,12039,12046,12050,12055],{"__ignoreMap":196},[201,12035,12036],{"class":203,"line":204},[201,12037,12038],{"class":207},"# 创建用户组和用户\n",[201,12040,12041,12043],{"class":203,"line":211},[201,12042,12021],{"class":214},[201,12044,12045],{"class":228}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[201,12047,12048],{"class":203,"line":232},[201,12049,779],{"emptyLinePlaceholder":394},[201,12051,12052],{"class":203,"line":245},[201,12053,12054],{"class":207},"# 修改目录权限\n",[201,12056,12057,12059],{"class":203,"line":256},[201,12058,12021],{"class":214},[201,12060,12061],{"class":228}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[564,12063,12065],{"id":12064},"_3-指定用户启动","3. 指定用户启动",[17,12067,12068,12069,12071],{},"执行 docker 时加 ",[198,12070,5852],{}," 参数：",[191,12073,12075],{"className":357,"code":12074,"language":359,"meta":196,"style":196},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[198,12076,12077],{"__ignoreMap":196},[201,12078,12079,12082,12084,12087,12090,12092,12095,12098,12100,12102,12105,12107,12109,12111,12113,12116,12118],{"class":203,"line":204},[201,12080,12081],{"class":291},"docker",[201,12083,1622],{"class":238},[201,12085,12086],{"class":221}," -it",[201,12088,12089],{"class":221}," --user",[201,12091,5441],{"class":214},[201,12093,12094],{"class":238},"app_use",[201,12096,12097],{"class":228},"r",[201,12099,3343],{"class":214},[201,12101,5441],{"class":214},[201,12103,12104],{"class":238},"app_imag",[201,12106,5477],{"class":228},[201,12108,3343],{"class":214},[201,12110,2400],{"class":238},[201,12112,644],{"class":214},[201,12114,12115],{"class":238},"image_versio",[201,12117,5447],{"class":228},[201,12119,649],{"class":214},[12,12121,12122],{"id":12122},"完整示例",[191,12124,12126],{"className":12007,"code":12125,"language":12009,"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,12127,12128,12136,12140,12145,12158,12162,12169,12173,12179,12184,12191,12196,12203,12207,12215,12219,12223,12230,12234,12242,12246,12253,12257,12271,12279,12283,12288,12295,12302,12306,12311,12319],{"__ignoreMap":196},[201,12129,12130,12133],{"class":203,"line":204},[201,12131,12132],{"class":214},"FROM",[201,12134,12135],{"class":228}," alpine:latest\n",[201,12137,12138],{"class":203,"line":211},[201,12139,779],{"emptyLinePlaceholder":394},[201,12141,12142],{"class":203,"line":232},[201,12143,12144],{"class":207},"# 国内源\n",[201,12146,12147,12149,12152,12155],{"class":203,"line":245},[201,12148,12021],{"class":214},[201,12150,12151],{"class":228}," sed -i ",[201,12153,12154],{"class":238},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[201,12156,12157],{"class":228}," \u002Fetc\u002Fapk\u002Frepositories\n",[201,12159,12160],{"class":203,"line":256},[201,12161,779],{"emptyLinePlaceholder":394},[201,12163,12164,12166],{"class":203,"line":267},[201,12165,12021],{"class":214},[201,12167,12168],{"class":228}," apk update --no-cache\n",[201,12170,12171],{"class":203,"line":273},[201,12172,12016],{"class":207},[201,12174,12175,12177],{"class":203,"line":819},[201,12176,12021],{"class":214},[201,12178,12024],{"class":228},[201,12180,12181],{"class":203,"line":825},[201,12182,12183],{"class":207},"# 调试\n",[201,12185,12186,12188],{"class":203,"line":830},[201,12187,12021],{"class":214},[201,12189,12190],{"class":228}," apk add --no-cache bash\n",[201,12192,12193],{"class":203,"line":836},[201,12194,12195],{"class":207},"# 设置时区\n",[201,12197,12198,12200],{"class":203,"line":853},[201,12199,12021],{"class":214},[201,12201,12202],{"class":228}," apk add --no-cache tzdata\n",[201,12204,12205],{"class":203,"line":858},[201,12206,779],{"emptyLinePlaceholder":394},[201,12208,12209,12212],{"class":203,"line":864},[201,12210,12211],{"class":214},"ENV",[201,12213,12214],{"class":228}," TZ=Asia\u002FShanghai\n",[201,12216,12217],{"class":203,"line":903},[201,12218,779],{"emptyLinePlaceholder":394},[201,12220,12221],{"class":203,"line":908},[201,12222,12038],{"class":207},[201,12224,12225,12227],{"class":203,"line":922},[201,12226,12021],{"class":214},[201,12228,12229],{"class":228}," addgroup -S glog && adduser -S -G glog glog\n",[201,12231,12232],{"class":203,"line":931},[201,12233,779],{"emptyLinePlaceholder":394},[201,12235,12236,12239],{"class":203,"line":946},[201,12237,12238],{"class":214},"COPY",[201,12240,12241],{"class":228}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[201,12243,12244],{"class":203,"line":951},[201,12245,779],{"emptyLinePlaceholder":394},[201,12247,12248,12250],{"class":203,"line":956},[201,12249,12021],{"class":214},[201,12251,12252],{"class":228}," mkdir \u002Fdata\n",[201,12254,12255],{"class":203,"line":1006},[201,12256,779],{"emptyLinePlaceholder":394},[201,12258,12259,12262,12265,12268],{"class":203,"line":1023},[201,12260,12261],{"class":214},"VOLUME",[201,12263,12264],{"class":228}," [ ",[201,12266,12267],{"class":238},"\"\u002Fdata\"",[201,12269,12270],{"class":228}," ]\n",[201,12272,12273,12276],{"class":203,"line":1035},[201,12274,12275],{"class":214},"WORKDIR",[201,12277,12278],{"class":228}," \u002Fdata\n",[201,12280,12281],{"class":203,"line":1046},[201,12282,779],{"emptyLinePlaceholder":394},[201,12284,12285],{"class":203,"line":1078},[201,12286,12287],{"class":207},"# 修改权限\n",[201,12289,12290,12292],{"class":203,"line":1084},[201,12291,12021],{"class":214},[201,12293,12294],{"class":228}," chown -R glog:glog \u002Fdata\n",[201,12296,12297,12299],{"class":203,"line":1104},[201,12298,12021],{"class":214},[201,12300,12301],{"class":228}," chown -R glog:glog \u002Fapp\n",[201,12303,12304],{"class":203,"line":6890},[201,12305,779],{"emptyLinePlaceholder":394},[201,12307,12308],{"class":203,"line":6896},[201,12309,12310],{"class":207},"# 指定用户启动\n",[201,12312,12313,12316],{"class":203,"line":6916},[201,12314,12315],{"class":214},"USER",[201,12317,12318],{"class":228}," glog\n",[201,12320,12321,12324,12326,12329,12331,12334,12336,12339],{"class":203,"line":6921},[201,12322,12323],{"class":214},"CMD",[201,12325,12264],{"class":228},[201,12327,12328],{"class":238},"\"\u002Fapp\u002Fglog_static_musl_1\"",[201,12330,338],{"class":228},[201,12332,12333],{"class":238},"\"logsweb\"",[201,12335,338],{"class":228},[201,12337,12338],{"class":238},"\".\u002Fconf.ini\"",[201,12340,12270],{"class":228},[12,12342,2054],{"id":2054},[17,12344,12345],{},"关键步骤：",[509,12347,12348,12354,12363,12369],{},[124,12349,12350,12353],{},[198,12351,12352],{},"apk add --no-cache shadow"," - 安装用户管理工具",[124,12355,12356,4652,12359,12362],{},[198,12357,12358],{},"addgroup",[198,12360,12361],{},"adduser"," - 创建用户",[124,12364,12365,12368],{},[198,12366,12367],{},"chown -R"," - 修改目录权限",[124,12370,12371,12374],{},[198,12372,12373],{},"USER \u003Cusername>"," - 指定运行用户",[17,12376,12377],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[438,12379,12380],{},"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":12382},[12383,12384,12389,12390],{"id":11992,"depth":211,"text":11992},{"id":10482,"depth":211,"text":10482,"children":12385},[12386,12387,12388],{"id":12000,"depth":232,"text":12001},{"id":12027,"depth":232,"text":12028},{"id":12064,"depth":232,"text":12065},{"id":12122,"depth":211,"text":12122},{"id":2054,"depth":211,"text":2054},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":11987,"description":12391},"Docker 实践笔记","posts\u002Fdocker-user-start\u002Findex",[12400,12401,12402],"Docker","运维","Linux","b4Uyw5ms87xr7NhpIkavHnNbT20oaEPtXz8k1w6UPj0",{"id":12405,"title":12406,"author":7,"body":12407,"description":13160,"difficulty":2117,"draft":452,"extension":453,"image":13161,"meta":13162,"navigation":394,"path":13163,"pinned":452,"published":13164,"seo":13165,"series":12397,"seriesOrder":204,"stem":13166,"tags":13167,"updated":13164,"__hash__":13168},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":9,"value":12408,"toc":13149},[12409,12412,12415,12418,12628,12631,12672,12674,12692,12695,12833,12836,12967,12970,12973,13031,13034,13146],[12,12410,12411],{"id":12411},"解决问题",[17,12413,12414],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[12,12416,12417],{"id":12417},"查阅资料",[191,12419,12421],{"className":357,"code":12420,"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,12422,12423,12428,12462,12466,12471,12497,12512,12526,12531,12543,12563,12571,12575,12580,12590,12600,12604,12612,12616],{"__ignoreMap":196},[201,12424,12425],{"class":203,"line":204},[201,12426,12427],{"class":207},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[201,12429,12430,12432,12434,12436,12439,12442,12444,12447,12450,12453,12456,12459],{"class":203,"line":211},[201,12431,12081],{"class":291},[201,12433,1622],{"class":238},[201,12435,12086],{"class":221},[201,12437,12438],{"class":221}," --rm",[201,12440,12441],{"class":221}," -v",[201,12443,1561],{"class":238},[201,12445,12446],{"class":228},"$PWD",[201,12448,12449],{"class":238},"\":\u002Fapp",[201,12451,12452],{"class":221}," -w",[201,12454,12455],{"class":238}," \u002Fapp",[201,12457,12458],{"class":238}," ubuntu:20.04",[201,12460,12461],{"class":238}," bash\n",[201,12463,12464],{"class":203,"line":232},[201,12465,779],{"emptyLinePlaceholder":394},[201,12467,12468],{"class":203,"line":245},[201,12469,12470],{"class":207},"# 在容器内执行以下操作：\n",[201,12472,12473,12476,12479,12481,12483,12485,12488,12491,12494],{"class":203,"line":256},[201,12474,12475],{"class":291},"apt-get",[201,12477,12478],{"class":238}," update",[201,12480,1616],{"class":228},[201,12482,12475],{"class":291},[201,12484,2290],{"class":238},[201,12486,12487],{"class":221}," -y",[201,12489,12490],{"class":238}," musl-tools",[201,12492,12493],{"class":238}," golang-go",[201,12495,12496],{"class":207},"  # 安装 musl 和 Go\n",[201,12498,12499,12501,12504,12506,12509],{"class":203,"line":267},[201,12500,215],{"class":214},[201,12502,12503],{"class":228}," CC",[201,12505,1029],{"class":214},[201,12507,12508],{"class":228},"musl-gcc  ",[201,12510,12511],{"class":207},"# 指定 musl 编译器\n",[201,12513,12514,12516,12519,12521,12523],{"class":203,"line":273},[201,12515,215],{"class":214},[201,12517,12518],{"class":228}," CGO_ENABLED",[201,12520,1029],{"class":214},[201,12522,3299],{"class":221},[201,12524,12525],{"class":207},"  # 启用 CGO\n",[201,12527,12528],{"class":203,"line":819},[201,12529,12530],{"class":207},"# 设置代理\n",[201,12532,12533,12535,12538,12540],{"class":203,"line":825},[201,12534,759],{"class":291},[201,12536,12537],{"class":238}," env",[201,12539,12452],{"class":221},[201,12541,12542],{"class":238}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[201,12544,12545,12547,12549,12552,12555,12557,12560],{"class":203,"line":830},[201,12546,759],{"class":291},[201,12548,1625],{"class":238},[201,12550,12551],{"class":221}," -ldflags",[201,12553,12554],{"class":238}," '-linkmode external -extldflags \"-static\"'",[201,12556,1679],{"class":221},[201,12558,12559],{"class":238}," myapp",[201,12561,12562],{"class":207},"  # 静态编译\n",[201,12564,12565,12568],{"class":203,"line":836},[201,12566,12567],{"class":221},"exit",[201,12569,12570],{"class":207},"  # 退出容器\n",[201,12572,12573],{"class":203,"line":853},[201,12574,779],{"emptyLinePlaceholder":394},[201,12576,12577],{"class":203,"line":858},[201,12578,12579],{"class":207},"# 验证结果（在宿主机执行）\n",[201,12581,12582,12585,12587],{"class":203,"line":864},[201,12583,12584],{"class":291},"file",[201,12586,12559],{"class":238},[201,12588,12589],{"class":207},"  # 应显示 \"statically linked\"\n",[201,12591,12592,12595,12597],{"class":203,"line":903},[201,12593,12594],{"class":291},"ldd",[201,12596,12559],{"class":238},[201,12598,12599],{"class":207},"   # 应显示 \"not a dynamic executable\"\n",[201,12601,12602],{"class":203,"line":908},[201,12603,779],{"emptyLinePlaceholder":394},[201,12605,12606,12609],{"class":203,"line":922},[201,12607,12608],{"class":291},"wget",[201,12610,12611],{"class":238}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[201,12613,12614],{"class":203,"line":931},[201,12615,779],{"emptyLinePlaceholder":394},[201,12617,12618,12620,12623,12626],{"class":203,"line":946},[201,12619,12211],{"class":291},[201,12621,12622],{"class":238}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[201,12624,12625],{"class":228},"$PATH",[201,12627,798],{"class":238},[564,12629,12630],{"id":12630},"遇到问题",[509,12632,12633,12658],{},[124,12634,12635,12638,12639,12642,12645,12646,12649,12651,12652,12654],{},[198,12636,12637],{},"go mod tidy"," 时报错 ",[198,12640,12641],{},"tls: failed to verify certificate: x509: certificate",[12643,12644],"br",{},"解决：",[198,12647,12648],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[12643,12650],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[12643,12653],{},[2419,12655],{"alt":12656,"src":12657},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[124,12659,12660,12661,12664,12665,12667,12668,12671],{},"直接安装 ",[198,12662,12663],{},"golang-go"," 其版本过低",[12643,12666],{},"解决: ",[198,12669,12670],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[12,12673,10742],{"id":10742},[509,12675,12676,12683,12686],{},[124,12677,12678,12679,12682],{},"下载 ",[198,12680,12681],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[124,12684,12685],{},"编写 Dockerfile",[124,12687,12688,12689],{},"执行 ",[198,12690,12691],{},"docker build -t go-build:v1 .",[17,12693,12694],{},"Dockerfile 内容：",[191,12696,12698],{"className":12007,"code":12697,"language":12009,"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,12699,12700,12707,12711,12722,12726,12733,12737,12747,12751,12758,12762,12767,12774,12778,12785,12790,12797,12801,12807,12811,12817,12821],{"__ignoreMap":196},[201,12701,12702,12704],{"class":203,"line":204},[201,12703,12132],{"class":214},[201,12705,12706],{"class":228}," ubuntu:20.04\n",[201,12708,12709],{"class":203,"line":211},[201,12710,779],{"emptyLinePlaceholder":394},[201,12712,12713,12716,12719],{"class":203,"line":232},[201,12714,12715],{"class":214},"LABEL",[201,12717,12718],{"class":228}," org.opencontainers.image.authors=",[201,12720,12721],{"class":238},"\"sutong\"\n",[201,12723,12724],{"class":203,"line":245},[201,12725,779],{"emptyLinePlaceholder":394},[201,12727,12728,12730],{"class":203,"line":256},[201,12729,12238],{"class":214},[201,12731,12732],{"class":228}," go\u002F \u002Fgo\u002F\n",[201,12734,12735],{"class":203,"line":267},[201,12736,779],{"emptyLinePlaceholder":394},[201,12738,12739,12741,12744],{"class":203,"line":273},[201,12740,12211],{"class":214},[201,12742,12743],{"class":228}," PATH=",[201,12745,12746],{"class":238},"\"\u002Fgo\u002Fbin:$PATH\"\n",[201,12748,12749],{"class":203,"line":819},[201,12750,779],{"emptyLinePlaceholder":394},[201,12752,12753,12755],{"class":203,"line":825},[201,12754,12021],{"class":214},[201,12756,12757],{"class":228}," apt-get update && apt-get install -y musl-tools\n",[201,12759,12760],{"class":203,"line":830},[201,12761,779],{"emptyLinePlaceholder":394},[201,12763,12764],{"class":203,"line":836},[201,12765,12766],{"class":207},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[201,12768,12769,12771],{"class":203,"line":853},[201,12770,12021],{"class":214},[201,12772,12773],{"class":228}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[201,12775,12776],{"class":203,"line":858},[201,12777,12511],{"class":207},[201,12779,12780,12782],{"class":203,"line":864},[201,12781,12211],{"class":214},[201,12783,12784],{"class":228}," CC=musl-gcc\n",[201,12786,12787],{"class":203,"line":903},[201,12788,12789],{"class":207},"# 启用 CGO\n",[201,12791,12792,12794],{"class":203,"line":908},[201,12793,12211],{"class":214},[201,12795,12796],{"class":228}," CGO_ENABLED=1\n",[201,12798,12799],{"class":203,"line":922},[201,12800,779],{"emptyLinePlaceholder":394},[201,12802,12803,12805],{"class":203,"line":931},[201,12804,12261],{"class":214},[201,12806,12278],{"class":228},[201,12808,12809],{"class":203,"line":946},[201,12810,779],{"emptyLinePlaceholder":394},[201,12812,12813,12815],{"class":203,"line":951},[201,12814,12275],{"class":214},[201,12816,12278],{"class":228},[201,12818,12819],{"class":203,"line":956},[201,12820,779],{"emptyLinePlaceholder":394},[201,12822,12823,12825,12828,12831],{"class":203,"line":1006},[201,12824,12323],{"class":214},[201,12826,12827],{"class":228}," [",[201,12829,12830],{"class":238},"\"\u002Fbin\u002Fbash\"",[201,12832,344],{"class":228},[564,12834,12835],{"id":12835},"构建",[191,12837,12839],{"className":357,"code":12838,"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,12840,12841,12846,12850,12855,12880,12884,12889,12921,12926,12930,12962],{"__ignoreMap":196},[201,12842,12843],{"class":203,"line":204},[201,12844,12845],{"class":207},"# docker build -t go-build:v1 .\n",[201,12847,12848],{"class":203,"line":211},[201,12849,779],{"emptyLinePlaceholder":394},[201,12851,12852],{"class":203,"line":232},[201,12853,12854],{"class":207},"# 直接执行编译 编译后删除\n",[201,12856,12857,12859,12861,12863,12865,12868,12871,12873,12875,12877],{"class":203,"line":245},[201,12858,12081],{"class":291},[201,12860,1622],{"class":238},[201,12862,12438],{"class":221},[201,12864,12441],{"class":221},[201,12866,12867],{"class":238}," .:\u002Fdata",[201,12869,12870],{"class":238}," go-build:v1",[201,12872,1674],{"class":238},[201,12874,1625],{"class":238},[201,12876,1679],{"class":221},[201,12878,12879],{"class":238}," app_musl\n",[201,12881,12882],{"class":203,"line":256},[201,12883,779],{"emptyLinePlaceholder":394},[201,12885,12886],{"class":203,"line":267},[201,12887,12888],{"class":207},"# 构建编译环境\n",[201,12890,12891,12893,12895,12897,12900,12902,12904,12906,12908,12911,12914,12916,12918],{"class":203,"line":273},[201,12892,12081],{"class":291},[201,12894,1622],{"class":238},[201,12896,12086],{"class":221},[201,12898,12899],{"class":221}," -d",[201,12901,12441],{"class":221},[201,12903,12867],{"class":238},[201,12905,5673],{"class":221},[201,12907,5441],{"class":214},[201,12909,12910],{"class":238},"容器名",[201,12912,12913],{"class":228},"称",[201,12915,3343],{"class":214},[201,12917,12870],{"class":238},[201,12919,12920],{"class":238}," \u002Fbin\u002Fbash\n",[201,12922,12923],{"class":203,"line":819},[201,12924,12925],{"class":207},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[201,12927,12928],{"class":203,"line":825},[201,12929,779],{"emptyLinePlaceholder":394},[201,12931,12932,12934,12937,12939,12941,12943,12945,12947,12949,12951,12953,12955,12958,12960],{"class":203,"line":830},[201,12933,12081],{"class":291},[201,12935,12936],{"class":238}," exec",[201,12938,12086],{"class":221},[201,12940,5441],{"class":214},[201,12942,12910],{"class":238},[201,12944,12913],{"class":228},[201,12946,3343],{"class":214},[201,12948,1674],{"class":238},[201,12950,1625],{"class":238},[201,12952,1679],{"class":221},[201,12954,5441],{"class":214},[201,12956,12957],{"class":238},"myap",[201,12959,17],{"class":228},[201,12961,649],{"class":214},[201,12963,12964],{"class":203,"line":836},[201,12965,12966],{"class":207},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[564,12968,12969],{"id":12969},"额外注意",[17,12971,12972],{},"若没科学上网需要设置代理：",[191,12974,12976],{"className":357,"code":12975,"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,12977,12978,12996,13006,13022],{"__ignoreMap":196},[201,12979,12980,12982,12984,12986,12988,12990,12992,12994],{"class":203,"line":204},[201,12981,12081],{"class":291},[201,12983,12936],{"class":238},[201,12985,12086],{"class":221},[201,12987,5441],{"class":214},[201,12989,12910],{"class":238},[201,12991,12913],{"class":228},[201,12993,3343],{"class":214},[201,12995,12920],{"class":238},[201,12997,12998,13000,13002,13004],{"class":203,"line":211},[201,12999,759],{"class":291},[201,13001,12537],{"class":238},[201,13003,12452],{"class":221},[201,13005,12542],{"class":238},[201,13007,13008,13010,13012,13014,13016,13018,13020],{"class":203,"line":232},[201,13009,759],{"class":291},[201,13011,1625],{"class":238},[201,13013,12551],{"class":221},[201,13015,12554],{"class":238},[201,13017,1679],{"class":221},[201,13019,12559],{"class":238},[201,13021,12562],{"class":207},[201,13023,13024,13026,13028],{"class":203,"line":245},[201,13025,759],{"class":291},[201,13027,1625],{"class":238},[201,13029,13030],{"class":207}," # musl 库编译\n",[564,13032,13033],{"id":13033},"运行环境示例",[191,13035,13037],{"className":12007,"code":13036,"language":12009,"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,13038,13039,13045,13049,13057,13061,13068,13072,13079,13086,13090,13097,13101,13108,13112,13132,13136,13141],{"__ignoreMap":196},[201,13040,13041,13043],{"class":203,"line":204},[201,13042,12132],{"class":214},[201,13044,12135],{"class":228},[201,13046,13047],{"class":203,"line":211},[201,13048,779],{"emptyLinePlaceholder":394},[201,13050,13051,13053,13055],{"class":203,"line":232},[201,13052,12715],{"class":214},[201,13054,12718],{"class":228},[201,13056,12721],{"class":238},[201,13058,13059],{"class":203,"line":245},[201,13060,779],{"emptyLinePlaceholder":394},[201,13062,13063,13065],{"class":203,"line":256},[201,13064,12238],{"class":214},[201,13066,13067],{"class":228}," glog_musl \u002Fapp\u002F\n",[201,13069,13070],{"class":203,"line":267},[201,13071,779],{"emptyLinePlaceholder":394},[201,13073,13074,13076],{"class":203,"line":273},[201,13075,12238],{"class":214},[201,13077,13078],{"class":228}," conf.ini \u002Fdata\u002F\n",[201,13080,13081,13083],{"class":203,"line":819},[201,13082,12238],{"class":214},[201,13084,13085],{"class":228}," ipname.ini \u002Fdata\u002F\n",[201,13087,13088],{"class":203,"line":825},[201,13089,779],{"emptyLinePlaceholder":394},[201,13091,13092,13094],{"class":203,"line":830},[201,13093,12275],{"class":214},[201,13095,13096],{"class":228}," \u002Fapp\n",[201,13098,13099],{"class":203,"line":836},[201,13100,779],{"emptyLinePlaceholder":394},[201,13102,13103,13105],{"class":203,"line":853},[201,13104,12021],{"class":214},[201,13106,13107],{"class":228}," chmod 777 \u002Fapp\u002Fglog_musl\n",[201,13109,13110],{"class":203,"line":858},[201,13111,779],{"emptyLinePlaceholder":394},[201,13113,13114,13116,13118,13121,13123,13125,13127,13130],{"class":203,"line":864},[201,13115,12323],{"class":214},[201,13117,12827],{"class":228},[201,13119,13120],{"class":238},"\".\u002Fglog_musl\"",[201,13122,338],{"class":228},[201,13124,12333],{"class":238},[201,13126,338],{"class":228},[201,13128,13129],{"class":238},"\"\u002Fdata\u002Fconf.ini\"",[201,13131,344],{"class":228},[201,13133,13134],{"class":203,"line":903},[201,13135,779],{"emptyLinePlaceholder":394},[201,13137,13138],{"class":203,"line":908},[201,13139,13140],{"class":207},"# docker build -t glog:v1 .\n",[201,13142,13143],{"class":203,"line":922},[201,13144,13145],{"class":207},"# docker run -p 6801:6801 glog:v1\n",[438,13147,13148],{},"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":13150},[13151,13152,13155],{"id":12411,"depth":211,"text":12411},{"id":12417,"depth":211,"text":12417,"children":13153},[13154],{"id":12630,"depth":232,"text":12630},{"id":10742,"depth":211,"text":10742,"children":13156},[13157,13158,13159],{"id":12835,"depth":232,"text":12835},{"id":12969,"depth":232,"text":12969},{"id":13033,"depth":232,"text":13033},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":12406,"description":13160},"posts\u002Fdocker-musl-build\u002Findex",[12400,2126,12401],"YLtdOOc-gMVDSNjE-ntKXz-7hff-VtvMB2dB5RXNi1o",1786887348660]