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