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