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