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