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