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