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