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