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