[{"data":1,"prerenderedAt":14896},["ShallowReactive",2],{"post-go-mcp-gin":3,"series-source-go-mcp-gin":2165},{"id":4,"title":5,"author":6,"body":7,"description":2146,"difficulty":2147,"draft":2148,"extension":2149,"image":2150,"meta":2151,"navigation":141,"path":2152,"pinned":2148,"published":2153,"seo":2154,"series":2155,"seriesOrder":90,"stem":2156,"tags":2157,"updated":2163,"__hash__":2164},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",null,{"type":8,"value":9,"toc":2127},"minimark",[10,15,19,22,26,34,41,44,47,69,72,77,112,116,232,245,249,423,426,650,654,665,764,771,775,1063,1066,1147,1151,1154,1157,1168,1172,1179,1550,1744,1747,1803,1806,1960,1963,1966,2039,2042,2114,2123],[11,12,14],"h2",{"id":13},"什么是-mcp","什么是 MCP",[16,17,18],"p",{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[16,20,21],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[11,23,25],{"id":24},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[16,27,28,29,33],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[30,31,32],"code",{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[16,35,36,37],{},"核心思路：",[38,39,40],"strong",{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[11,42,43],{"id":43},"需求分析",[16,45,46],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[48,49,50,57,63],"ol",{},[51,52,53,56],"li",{},[38,54,55],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[51,58,59,62],{},[38,60,61],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[51,64,65,68],{},[38,66,67],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[11,70,71],{"id":71},"实现步骤",[73,74,76],"h3",{"id":75},"_1-安装依赖","1. 安装依赖",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-bash shiki shiki-themes github-light github-dark","go get github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\ngo get github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n","bash","",[30,85,86,102],{"__ignoreMap":83},[87,88,91,95,99],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"sw2iP","go",[87,96,98],{"class":97},"scXbn"," get",[87,100,101],{"class":97}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[87,103,105,107,109],{"class":89,"line":104},2,[87,106,94],{"class":93},[87,108,98],{"class":97},[87,110,111],{"class":97}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[73,113,115],{"id":114},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[78,117,120],{"className":118,"code":119,"language":94,"meta":83,"style":83},"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",[30,121,122,137,143,162,175,187,199,209,226],{"__ignoreMap":83},[87,123,124,128,131,134],{"class":89,"line":90},[87,125,127],{"class":126},"siTax","import",[87,129,130],{"class":97}," \"",[87,132,133],{"class":93},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[87,135,136],{"class":97},"\"\n",[87,138,139],{"class":89,"line":104},[87,140,142],{"emptyLinePlaceholder":141},true,"\n",[87,144,146,150,153,156,159],{"class":89,"line":145},3,[87,147,149],{"class":148},"sIX_F","mcpServer ",[87,151,152],{"class":126},":=",[87,154,155],{"class":148}," server.",[87,157,158],{"class":93},"NewMCPServer",[87,160,161],{"class":148},"(\n",[87,163,165,168,171],{"class":89,"line":164},4,[87,166,167],{"class":97},"    \"my-app\"",[87,169,170],{"class":148},",    ",[87,172,174],{"class":173},"sHbNN","\u002F\u002F 服务器名称\n",[87,176,178,181,184],{"class":89,"line":177},5,[87,179,180],{"class":97},"    \"1.0.0\"",[87,182,183],{"class":148},",     ",[87,185,186],{"class":173},"\u002F\u002F 版本\n",[87,188,190,193,196],{"class":89,"line":189},6,[87,191,192],{"class":148},"    server.",[87,194,195],{"class":93},"WithLogging",[87,197,198],{"class":148},"(),\n",[87,200,202,204,207],{"class":89,"line":201},7,[87,203,192],{"class":148},[87,205,206],{"class":93},"WithRecovery",[87,208,198],{"class":148},[87,210,212,214,217,220,223],{"class":89,"line":211},8,[87,213,192],{"class":148},[87,215,216],{"class":93},"WithInstructions",[87,218,219],{"class":148},"(",[87,221,222],{"class":97},"\"这是一个 MCP 服务器示例\"",[87,224,225],{"class":148},"),\n",[87,227,229],{"class":89,"line":228},9,[87,230,231],{"class":148},")\n",[16,233,234,236,237,240,241,244],{},[30,235,158],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[30,238,239],{},"tools\u002Flist","）和工具调用（",[30,242,243],{},"tools\u002Fcall","）的协议逻辑。",[73,246,248],{"id":247},"_3-注册工具","3. 注册工具",[78,250,252],{"className":118,"code":251,"language":94,"meta":83,"style":83},"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",[30,253,254,265,269,274,295,310,314,318,323,388,394,417],{"__ignoreMap":83},[87,255,256,258,260,263],{"class":89,"line":90},[87,257,127],{"class":126},[87,259,130],{"class":97},[87,261,262],{"class":93},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[87,264,136],{"class":97},[87,266,267],{"class":89,"line":104},[87,268,142],{"emptyLinePlaceholder":141},[87,270,271],{"class":89,"line":145},[87,272,273],{"class":173},"\u002F\u002F 定义工具\n",[87,275,276,279,281,284,287,289,292],{"class":89,"line":164},[87,277,278],{"class":148},"listTool ",[87,280,152],{"class":126},[87,282,283],{"class":148}," mcp.",[87,285,286],{"class":93},"NewTool",[87,288,219],{"class":148},[87,290,291],{"class":97},"\"list_items\"",[87,293,294],{"class":148},",\n",[87,296,297,300,303,305,308],{"class":89,"line":177},[87,298,299],{"class":148},"    mcp.",[87,301,302],{"class":93},"WithDescription",[87,304,219],{"class":148},[87,306,307],{"class":97},"\"列出所有项目\"",[87,309,225],{"class":148},[87,311,312],{"class":89,"line":189},[87,313,231],{"class":148},[87,315,316],{"class":89,"line":201},[87,317,142],{"emptyLinePlaceholder":141},[87,319,320],{"class":89,"line":211},[87,321,322],{"class":173},"\u002F\u002F 注册处理器\n",[87,324,325,328,331,334,337,339,343,346,349,352,355,358,361,363,366,369,372,375,377,380,382,385],{"class":89,"line":228},[87,326,327],{"class":148},"mcpServer.",[87,329,330],{"class":93},"AddTool",[87,332,333],{"class":148},"(listTool, ",[87,335,336],{"class":126},"func",[87,338,219],{"class":148},[87,340,342],{"class":341},"sPK6S","ctx",[87,344,345],{"class":93}," context",[87,347,348],{"class":148},".",[87,350,351],{"class":93},"Context",[87,353,354],{"class":148},", ",[87,356,357],{"class":341},"req",[87,359,360],{"class":93}," mcp",[87,362,348],{"class":148},[87,364,365],{"class":93},"CallToolRequest",[87,367,368],{"class":148},") (",[87,370,371],{"class":126},"*",[87,373,374],{"class":93},"mcp",[87,376,348],{"class":148},[87,378,379],{"class":93},"CallToolResult",[87,381,354],{"class":148},[87,383,384],{"class":126},"error",[87,386,387],{"class":148},") {\n",[87,389,391],{"class":89,"line":390},10,[87,392,393],{"class":173},"    \u002F\u002F 业务逻辑\n",[87,395,397,400,402,405,407,410,413],{"class":89,"line":396},11,[87,398,399],{"class":126},"    return",[87,401,283],{"class":148},[87,403,404],{"class":93},"NewToolResultText",[87,406,219],{"class":148},[87,408,409],{"class":97},"\"返回结果\"",[87,411,412],{"class":148},"), ",[87,414,416],{"class":415},"suQ91","nil\n",[87,418,420],{"class":89,"line":419},12,[87,421,422],{"class":148},"})\n",[16,424,425],{},"带参数的工具：",[78,427,429],{"className":118,"code":428,"language":94,"meta":83,"style":83},"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",[30,430,431,449,462,476,486,500,505,509,513,560,579,596,615,621,627,645],{"__ignoreMap":83},[87,432,433,436,438,440,442,444,447],{"class":89,"line":90},[87,434,435],{"class":148},"getTool ",[87,437,152],{"class":126},[87,439,283],{"class":148},[87,441,286],{"class":93},[87,443,219],{"class":148},[87,445,446],{"class":97},"\"get_item\"",[87,448,294],{"class":148},[87,450,451,453,455,457,460],{"class":89,"line":104},[87,452,299],{"class":148},[87,454,302],{"class":93},[87,456,219],{"class":148},[87,458,459],{"class":97},"\"获取指定项目的详情\"",[87,461,225],{"class":148},[87,463,464,466,469,471,474],{"class":89,"line":145},[87,465,299],{"class":148},[87,467,468],{"class":93},"WithString",[87,470,219],{"class":148},[87,472,473],{"class":97},"\"item_id\"",[87,475,294],{"class":148},[87,477,478,481,484],{"class":89,"line":164},[87,479,480],{"class":148},"        mcp.",[87,482,483],{"class":93},"Required",[87,485,198],{"class":148},[87,487,488,490,493,495,498],{"class":89,"line":177},[87,489,480],{"class":148},[87,491,492],{"class":93},"Description",[87,494,219],{"class":148},[87,496,497],{"class":97},"\"项目 ID\"",[87,499,225],{"class":148},[87,501,502],{"class":89,"line":189},[87,503,504],{"class":148},"    ),\n",[87,506,507],{"class":89,"line":201},[87,508,231],{"class":148},[87,510,511],{"class":89,"line":211},[87,512,142],{"emptyLinePlaceholder":141},[87,514,515,517,519,522,524,526,528,530,532,534,536,538,540,542,544,546,548,550,552,554,556,558],{"class":89,"line":228},[87,516,327],{"class":148},[87,518,330],{"class":93},[87,520,521],{"class":148},"(getTool, ",[87,523,336],{"class":126},[87,525,219],{"class":148},[87,527,342],{"class":341},[87,529,345],{"class":93},[87,531,348],{"class":148},[87,533,351],{"class":93},[87,535,354],{"class":148},[87,537,357],{"class":341},[87,539,360],{"class":93},[87,541,348],{"class":148},[87,543,365],{"class":93},[87,545,368],{"class":148},[87,547,371],{"class":126},[87,549,374],{"class":93},[87,551,348],{"class":148},[87,553,379],{"class":93},[87,555,354],{"class":148},[87,557,384],{"class":126},[87,559,387],{"class":148},[87,561,562,565,567,570,573,575,577],{"class":89,"line":390},[87,563,564],{"class":148},"    itemID, err ",[87,566,152],{"class":126},[87,568,569],{"class":148}," req.",[87,571,572],{"class":93},"RequireString",[87,574,219],{"class":148},[87,576,473],{"class":97},[87,578,231],{"class":148},[87,580,581,584,587,590,593],{"class":89,"line":396},[87,582,583],{"class":126},"    if",[87,585,586],{"class":148}," err ",[87,588,589],{"class":126},"!=",[87,591,592],{"class":415}," nil",[87,594,595],{"class":148}," {\n",[87,597,598,601,603,606,608,611,613],{"class":89,"line":419},[87,599,600],{"class":126},"        return",[87,602,283],{"class":148},[87,604,605],{"class":93},"NewToolResultError",[87,607,219],{"class":148},[87,609,610],{"class":97},"\"缺少 item_id 参数\"",[87,612,412],{"class":148},[87,614,416],{"class":415},[87,616,618],{"class":89,"line":617},13,[87,619,620],{"class":148},"    }\n",[87,622,624],{"class":89,"line":623},14,[87,625,626],{"class":173},"    \u002F\u002F 查询并返回\n",[87,628,630,632,634,636,638,641,643],{"class":89,"line":629},15,[87,631,399],{"class":126},[87,633,283],{"class":148},[87,635,404],{"class":93},[87,637,219],{"class":148},[87,639,640],{"class":97},"\"项目详情...\"",[87,642,412],{"class":148},[87,644,416],{"class":415},[87,646,648],{"class":89,"line":647},16,[87,649,422],{"class":148},[73,651,653],{"id":652},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[16,655,656,657,660,661,664],{},"关键一步：用 ",[30,658,659],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[30,662,663],{},"gin.WrapH"," 适配 Gin。",[78,666,668],{"className":118,"code":667,"language":94,"meta":83,"style":83},"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",[30,669,670,680,684,699,713,730,734,738,743],{"__ignoreMap":83},[87,671,672,674,676,678],{"class":89,"line":90},[87,673,127],{"class":126},[87,675,130],{"class":97},[87,677,133],{"class":93},[87,679,136],{"class":97},[87,681,682],{"class":89,"line":104},[87,683,142],{"emptyLinePlaceholder":141},[87,685,686,689,691,693,696],{"class":89,"line":145},[87,687,688],{"class":148},"mcpHTTPServer ",[87,690,152],{"class":126},[87,692,155],{"class":148},[87,694,695],{"class":93},"NewStreamableHTTPServer",[87,697,698],{"class":148},"(mcpServer,\n",[87,700,701,703,706,708,711],{"class":89,"line":164},[87,702,192],{"class":148},[87,704,705],{"class":93},"WithEndpointPath",[87,707,219],{"class":148},[87,709,710],{"class":97},"\"\u002Fmcp\"",[87,712,225],{"class":148},[87,714,715,717,720,722,725,727],{"class":89,"line":177},[87,716,192],{"class":148},[87,718,719],{"class":93},"WithHeartbeatInterval",[87,721,219],{"class":148},[87,723,724],{"class":415},"30",[87,726,371],{"class":126},[87,728,729],{"class":148},"time.Second),\n",[87,731,732],{"class":89,"line":189},[87,733,231],{"class":148},[87,735,736],{"class":89,"line":201},[87,737,142],{"emptyLinePlaceholder":141},[87,739,740],{"class":89,"line":211},[87,741,742],{"class":173},"\u002F\u002F 挂载到 Gin\n",[87,744,745,748,751,753,755,758,761],{"class":89,"line":228},[87,746,747],{"class":148},"r.",[87,749,750],{"class":93},"Any",[87,752,219],{"class":148},[87,754,710],{"class":97},[87,756,757],{"class":148},", gin.",[87,759,760],{"class":93},"WrapH",[87,762,763],{"class":148},"(mcpHTTPServer))\n",[16,765,766,767,770],{},"就这样，",[30,768,769],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[73,772,774],{"id":773},"_5-完整的服务启动代码","5. 完整的服务启动代码",[78,776,778],{"className":118,"code":777,"language":94,"meta":83,"style":83},"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",[30,779,780,790,806,810,815,838,847,855,860,864,869,877,881,886,899,911,925,930,949,954,960,994,1031,1037,1042,1057],{"__ignoreMap":83},[87,781,782,784,787],{"class":89,"line":90},[87,783,336],{"class":126},[87,785,786],{"class":93}," main",[87,788,789],{"class":148},"() {\n",[87,791,792,795,797,800,803],{"class":89,"line":104},[87,793,794],{"class":148},"    r ",[87,796,152],{"class":126},[87,798,799],{"class":148}," gin.",[87,801,802],{"class":93},"Default",[87,804,805],{"class":148},"()\n",[87,807,808],{"class":89,"line":145},[87,809,142],{"emptyLinePlaceholder":141},[87,811,812],{"class":89,"line":164},[87,813,814],{"class":173},"    \u002F\u002F 创建 MCP 服务器\n",[87,816,817,820,822,824,826,828,831,833,836],{"class":89,"line":177},[87,818,819],{"class":148},"    mcpServer ",[87,821,152],{"class":126},[87,823,155],{"class":148},[87,825,158],{"class":93},[87,827,219],{"class":148},[87,829,830],{"class":97},"\"my-app\"",[87,832,354],{"class":148},[87,834,835],{"class":97},"\"1.0.0\"",[87,837,294],{"class":148},[87,839,840,843,845],{"class":89,"line":189},[87,841,842],{"class":148},"        server.",[87,844,195],{"class":93},[87,846,198],{"class":148},[87,848,849,851,853],{"class":89,"line":201},[87,850,842],{"class":148},[87,852,206],{"class":93},[87,854,198],{"class":148},[87,856,857],{"class":89,"line":211},[87,858,859],{"class":148},"    )\n",[87,861,862],{"class":89,"line":228},[87,863,142],{"emptyLinePlaceholder":141},[87,865,866],{"class":89,"line":390},[87,867,868],{"class":173},"    \u002F\u002F 注册工具\n",[87,870,871,874],{"class":89,"line":396},[87,872,873],{"class":93},"    registerTools",[87,875,876],{"class":148},"(mcpServer)\n",[87,878,879],{"class":89,"line":419},[87,880,142],{"emptyLinePlaceholder":141},[87,882,883],{"class":89,"line":617},[87,884,885],{"class":173},"    \u002F\u002F 挂载到 Gin\n",[87,887,888,891,893,895,897],{"class":89,"line":623},[87,889,890],{"class":148},"    mcpHTTP ",[87,892,152],{"class":126},[87,894,155],{"class":148},[87,896,695],{"class":93},[87,898,698],{"class":148},[87,900,901,903,905,907,909],{"class":89,"line":629},[87,902,842],{"class":148},[87,904,705],{"class":93},[87,906,219],{"class":148},[87,908,710],{"class":97},[87,910,225],{"class":148},[87,912,913,915,917,919,921,923],{"class":89,"line":647},[87,914,842],{"class":148},[87,916,719],{"class":93},[87,918,219],{"class":148},[87,920,724],{"class":415},[87,922,371],{"class":126},[87,924,729],{"class":148},[87,926,928],{"class":89,"line":927},17,[87,929,859],{"class":148},[87,931,933,936,938,940,942,944,946],{"class":89,"line":932},18,[87,934,935],{"class":148},"    r.",[87,937,750],{"class":93},[87,939,219],{"class":148},[87,941,710],{"class":97},[87,943,757],{"class":148},[87,945,760],{"class":93},[87,947,948],{"class":148},"(mcpHTTP))\n",[87,950,952],{"class":89,"line":951},19,[87,953,142],{"emptyLinePlaceholder":141},[87,955,957],{"class":89,"line":956},20,[87,958,959],{"class":173},"    \u002F\u002F 其他业务路由\n",[87,961,963,965,968,970,973,975,977,979,982,985,988,990,992],{"class":89,"line":962},21,[87,964,935],{"class":148},[87,966,967],{"class":93},"GET",[87,969,219],{"class":148},[87,971,972],{"class":97},"\"\u002Fapi\u002Fhealth\"",[87,974,354],{"class":148},[87,976,336],{"class":126},[87,978,219],{"class":148},[87,980,981],{"class":341},"c",[87,983,984],{"class":126}," *",[87,986,987],{"class":93},"gin",[87,989,348],{"class":148},[87,991,351],{"class":93},[87,993,387],{"class":148},[87,995,997,1000,1003,1005,1008,1010,1012,1014,1017,1020,1023,1026,1029],{"class":89,"line":996},22,[87,998,999],{"class":148},"        c.",[87,1001,1002],{"class":93},"JSON",[87,1004,219],{"class":148},[87,1006,1007],{"class":415},"200",[87,1009,354],{"class":148},[87,1011,987],{"class":93},[87,1013,348],{"class":148},[87,1015,1016],{"class":93},"H",[87,1018,1019],{"class":148},"{",[87,1021,1022],{"class":97},"\"status\"",[87,1024,1025],{"class":148},": ",[87,1027,1028],{"class":97},"\"ok\"",[87,1030,422],{"class":148},[87,1032,1034],{"class":89,"line":1033},23,[87,1035,1036],{"class":148},"    })\n",[87,1038,1040],{"class":89,"line":1039},24,[87,1041,142],{"emptyLinePlaceholder":141},[87,1043,1045,1047,1050,1052,1055],{"class":89,"line":1044},25,[87,1046,935],{"class":148},[87,1048,1049],{"class":93},"Run",[87,1051,219],{"class":148},[87,1053,1054],{"class":97},"\":8080\"",[87,1056,231],{"class":148},[87,1058,1060],{"class":89,"line":1059},26,[87,1061,1062],{"class":148},"}\n",[16,1064,1065],{},"此时启动服务，用 curl 测试：",[78,1067,1069],{"className":80,"code":1068,"language":82,"meta":83,"style":83},"# 发现工具列表\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",[30,1070,1071,1076,1093,1103,1111,1115,1120,1132,1140],{"__ignoreMap":83},[87,1072,1073],{"class":89,"line":90},[87,1074,1075],{"class":173},"# 发现工具列表\n",[87,1077,1078,1081,1084,1087,1090],{"class":89,"line":104},[87,1079,1080],{"class":93},"curl",[87,1082,1083],{"class":415}," -X",[87,1085,1086],{"class":97}," POST",[87,1088,1089],{"class":97}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[87,1091,1092],{"class":415}," \\\n",[87,1094,1095,1098,1101],{"class":89,"line":145},[87,1096,1097],{"class":415},"  -H",[87,1099,1100],{"class":97}," \"Content-Type: application\u002Fjson\"",[87,1102,1092],{"class":415},[87,1104,1105,1108],{"class":89,"line":164},[87,1106,1107],{"class":415},"  -d",[87,1109,1110],{"class":97}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[87,1112,1113],{"class":89,"line":177},[87,1114,142],{"emptyLinePlaceholder":141},[87,1116,1117],{"class":89,"line":189},[87,1118,1119],{"class":173},"# 调用工具\n",[87,1121,1122,1124,1126,1128,1130],{"class":89,"line":201},[87,1123,1080],{"class":93},[87,1125,1083],{"class":415},[87,1127,1086],{"class":97},[87,1129,1089],{"class":97},[87,1131,1092],{"class":415},[87,1133,1134,1136,1138],{"class":89,"line":211},[87,1135,1097],{"class":415},[87,1137,1100],{"class":97},[87,1139,1092],{"class":415},[87,1141,1142,1144],{"class":89,"line":228},[87,1143,1107],{"class":415},[87,1145,1146],{"class":97}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[11,1148,1150],{"id":1149},"拓展接入鉴权","拓展：接入鉴权",[16,1152,1153],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[73,1155,1156],{"id":1156},"问题",[16,1158,1159,1160,1163,1164,1167],{},"MCP 的工具处理器签名为 ",[30,1161,1162],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[30,1165,1166],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[73,1169,1171],{"id":1170},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[16,1173,1174,1175,1178],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[30,1176,1177],{},"request.Context","。",[78,1180,1182],{"className":118,"code":1181,"language":94,"meta":83,"style":83},"\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",[30,1183,1184,1189,1218,1239,1259,1275,1308,1313,1318,1322,1348,1389,1418,1422,1426,1430,1451,1463,1492,1496,1500,1504,1519,1533,1542,1546],{"__ignoreMap":83},[87,1185,1186],{"class":89,"line":90},[87,1187,1188],{"class":173},"\u002F\u002F Gin 鉴权中间件\n",[87,1190,1191,1193,1196,1198,1201,1203,1206,1209,1211,1213,1216],{"class":89,"line":104},[87,1192,336],{"class":126},[87,1194,1195],{"class":93}," AuthRequired",[87,1197,219],{"class":148},[87,1199,1200],{"class":341},"authService",[87,1202,984],{"class":126},[87,1204,1205],{"class":93},"AuthService",[87,1207,1208],{"class":148},") ",[87,1210,987],{"class":93},[87,1212,348],{"class":148},[87,1214,1215],{"class":93},"HandlerFunc",[87,1217,595],{"class":148},[87,1219,1220,1222,1225,1227,1229,1231,1233,1235,1237],{"class":89,"line":145},[87,1221,399],{"class":126},[87,1223,1224],{"class":126}," func",[87,1226,219],{"class":148},[87,1228,981],{"class":341},[87,1230,984],{"class":126},[87,1232,987],{"class":93},[87,1234,348],{"class":148},[87,1236,351],{"class":93},[87,1238,387],{"class":148},[87,1240,1241,1244,1246,1249,1252,1254,1257],{"class":89,"line":164},[87,1242,1243],{"class":148},"        authHeader ",[87,1245,152],{"class":126},[87,1247,1248],{"class":148}," c.",[87,1250,1251],{"class":93},"GetHeader",[87,1253,219],{"class":148},[87,1255,1256],{"class":97},"\"Authorization\"",[87,1258,231],{"class":148},[87,1260,1261,1264,1267,1270,1273],{"class":89,"line":177},[87,1262,1263],{"class":126},"        if",[87,1265,1266],{"class":148}," authHeader ",[87,1268,1269],{"class":126},"==",[87,1271,1272],{"class":97}," \"\"",[87,1274,595],{"class":148},[87,1276,1277,1280,1283,1285,1288,1290,1292,1294,1296,1298,1301,1303,1306],{"class":89,"line":189},[87,1278,1279],{"class":148},"            c.",[87,1281,1282],{"class":93},"AbortWithStatusJSON",[87,1284,219],{"class":148},[87,1286,1287],{"class":415},"401",[87,1289,354],{"class":148},[87,1291,987],{"class":93},[87,1293,348],{"class":148},[87,1295,1016],{"class":93},[87,1297,1019],{"class":148},[87,1299,1300],{"class":97},"\"message\"",[87,1302,1025],{"class":148},[87,1304,1305],{"class":97},"\"缺少认证令牌\"",[87,1307,422],{"class":148},[87,1309,1310],{"class":89,"line":201},[87,1311,1312],{"class":126},"            return\n",[87,1314,1315],{"class":89,"line":211},[87,1316,1317],{"class":148},"        }\n",[87,1319,1320],{"class":89,"line":228},[87,1321,142],{"emptyLinePlaceholder":141},[87,1323,1324,1327,1329,1332,1335,1338,1341,1343,1346],{"class":89,"line":390},[87,1325,1326],{"class":148},"        parts ",[87,1328,152],{"class":126},[87,1330,1331],{"class":148}," strings.",[87,1333,1334],{"class":93},"SplitN",[87,1336,1337],{"class":148},"(authHeader, ",[87,1339,1340],{"class":97},"\" \"",[87,1342,354],{"class":148},[87,1344,1345],{"class":415},"2",[87,1347,231],{"class":148},[87,1349,1350,1352,1355,1358,1360,1363,1366,1369,1372,1375,1378,1381,1384,1387],{"class":89,"line":396},[87,1351,1263],{"class":126},[87,1353,1354],{"class":93}," len",[87,1356,1357],{"class":148},"(parts) ",[87,1359,589],{"class":126},[87,1361,1362],{"class":415}," 2",[87,1364,1365],{"class":126}," ||",[87,1367,1368],{"class":126}," !",[87,1370,1371],{"class":148},"strings.",[87,1373,1374],{"class":93},"EqualFold",[87,1376,1377],{"class":148},"(parts[",[87,1379,1380],{"class":415},"0",[87,1382,1383],{"class":148},"], ",[87,1385,1386],{"class":97},"\"Bearer\"",[87,1388,387],{"class":148},[87,1390,1391,1393,1395,1397,1399,1401,1403,1405,1407,1409,1411,1413,1416],{"class":89,"line":419},[87,1392,1279],{"class":148},[87,1394,1282],{"class":93},[87,1396,219],{"class":148},[87,1398,1287],{"class":415},[87,1400,354],{"class":148},[87,1402,987],{"class":93},[87,1404,348],{"class":148},[87,1406,1016],{"class":93},[87,1408,1019],{"class":148},[87,1410,1300],{"class":97},[87,1412,1025],{"class":148},[87,1414,1415],{"class":97},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[87,1417,422],{"class":148},[87,1419,1420],{"class":89,"line":617},[87,1421,1312],{"class":126},[87,1423,1424],{"class":89,"line":623},[87,1425,1317],{"class":148},[87,1427,1428],{"class":89,"line":629},[87,1429,142],{"emptyLinePlaceholder":141},[87,1431,1432,1435,1437,1440,1443,1445,1448],{"class":89,"line":647},[87,1433,1434],{"class":148},"        user, err ",[87,1436,152],{"class":126},[87,1438,1439],{"class":148}," authService.",[87,1441,1442],{"class":93},"GetUserByToken",[87,1444,1377],{"class":148},[87,1446,1447],{"class":415},"1",[87,1449,1450],{"class":148},"])\n",[87,1452,1453,1455,1457,1459,1461],{"class":89,"line":927},[87,1454,1263],{"class":126},[87,1456,586],{"class":148},[87,1458,589],{"class":126},[87,1460,592],{"class":415},[87,1462,595],{"class":148},[87,1464,1465,1467,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1490],{"class":89,"line":932},[87,1466,1279],{"class":148},[87,1468,1282],{"class":93},[87,1470,219],{"class":148},[87,1472,1287],{"class":415},[87,1474,354],{"class":148},[87,1476,987],{"class":93},[87,1478,348],{"class":148},[87,1480,1016],{"class":93},[87,1482,1019],{"class":148},[87,1484,1300],{"class":97},[87,1486,1025],{"class":148},[87,1488,1489],{"class":97},"\"无效的认证令牌\"",[87,1491,422],{"class":148},[87,1493,1494],{"class":89,"line":951},[87,1495,1312],{"class":126},[87,1497,1498],{"class":89,"line":956},[87,1499,1317],{"class":148},[87,1501,1502],{"class":89,"line":962},[87,1503,142],{"emptyLinePlaceholder":141},[87,1505,1506,1508,1511,1513,1516],{"class":89,"line":996},[87,1507,999],{"class":148},[87,1509,1510],{"class":93},"Set",[87,1512,219],{"class":148},[87,1514,1515],{"class":97},"\"userID\"",[87,1517,1518],{"class":148},", user.ID)\n",[87,1520,1521,1523,1525,1527,1530],{"class":89,"line":1033},[87,1522,999],{"class":148},[87,1524,1510],{"class":93},[87,1526,219],{"class":148},[87,1528,1529],{"class":97},"\"username\"",[87,1531,1532],{"class":148},", user.Username)\n",[87,1534,1535,1537,1540],{"class":89,"line":1039},[87,1536,999],{"class":148},[87,1538,1539],{"class":93},"Next",[87,1541,805],{"class":148},[87,1543,1544],{"class":89,"line":1044},[87,1545,620],{"class":148},[87,1547,1548],{"class":89,"line":1059},[87,1549,1062],{"class":148},[78,1551,1553],{"className":118,"code":1552,"language":94,"meta":83,"style":83},"\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",[30,1554,1555,1560,1571,1579,1593,1605,1609,1613,1631,1651,1665,1691,1713,1728,1736,1740],{"__ignoreMap":83},[87,1556,1557],{"class":89,"line":90},[87,1558,1559],{"class":173},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[87,1561,1562,1565,1568],{"class":89,"line":104},[87,1563,1564],{"class":126},"type",[87,1566,1567],{"class":93}," ctxKey",[87,1569,1570],{"class":126}," string\n",[87,1572,1573,1576],{"class":89,"line":145},[87,1574,1575],{"class":126},"const",[87,1577,1578],{"class":148}," (\n",[87,1580,1581,1584,1587,1590],{"class":89,"line":164},[87,1582,1583],{"class":415},"    ctxUserID",[87,1585,1586],{"class":93},"   ctxKey",[87,1588,1589],{"class":126}," =",[87,1591,1592],{"class":97}," \"userID\"\n",[87,1594,1595,1598,1600,1602],{"class":89,"line":177},[87,1596,1597],{"class":415},"    ctxUsername",[87,1599,1567],{"class":93},[87,1601,1589],{"class":126},[87,1603,1604],{"class":97}," \"username\"\n",[87,1606,1607],{"class":89,"line":189},[87,1608,231],{"class":148},[87,1610,1611],{"class":89,"line":201},[87,1612,142],{"emptyLinePlaceholder":141},[87,1614,1615,1617,1620,1623,1625,1627,1629],{"class":89,"line":211},[87,1616,336],{"class":126},[87,1618,1619],{"class":93}," WithUserContext",[87,1621,1622],{"class":148},"() ",[87,1624,987],{"class":93},[87,1626,348],{"class":148},[87,1628,1215],{"class":93},[87,1630,595],{"class":148},[87,1632,1633,1635,1637,1639,1641,1643,1645,1647,1649],{"class":89,"line":228},[87,1634,399],{"class":126},[87,1636,1224],{"class":126},[87,1638,219],{"class":148},[87,1640,981],{"class":341},[87,1642,984],{"class":126},[87,1644,987],{"class":93},[87,1646,348],{"class":148},[87,1648,351],{"class":93},[87,1650,387],{"class":148},[87,1652,1653,1656,1658,1661,1663],{"class":89,"line":390},[87,1654,1655],{"class":148},"        ctx ",[87,1657,152],{"class":126},[87,1659,1660],{"class":148}," c.Request.",[87,1662,351],{"class":93},[87,1664,805],{"class":148},[87,1666,1667,1669,1672,1675,1678,1681,1684,1686,1688],{"class":89,"line":396},[87,1668,1655],{"class":148},[87,1670,1671],{"class":126},"=",[87,1673,1674],{"class":148}," context.",[87,1676,1677],{"class":93},"WithValue",[87,1679,1680],{"class":148},"(ctx, ctxUserID, c.",[87,1682,1683],{"class":93},"GetUint",[87,1685,219],{"class":148},[87,1687,1515],{"class":97},[87,1689,1690],{"class":148},"))\n",[87,1692,1693,1695,1697,1699,1701,1704,1707,1709,1711],{"class":89,"line":419},[87,1694,1655],{"class":148},[87,1696,1671],{"class":126},[87,1698,1674],{"class":148},[87,1700,1677],{"class":93},[87,1702,1703],{"class":148},"(ctx, ctxUsername, c.",[87,1705,1706],{"class":93},"GetString",[87,1708,219],{"class":148},[87,1710,1529],{"class":97},[87,1712,1690],{"class":148},[87,1714,1715,1718,1720,1722,1725],{"class":89,"line":617},[87,1716,1717],{"class":148},"        c.Request ",[87,1719,1671],{"class":126},[87,1721,1660],{"class":148},[87,1723,1724],{"class":93},"WithContext",[87,1726,1727],{"class":148},"(ctx)\n",[87,1729,1730,1732,1734],{"class":89,"line":623},[87,1731,999],{"class":148},[87,1733,1539],{"class":93},[87,1735,805],{"class":148},[87,1737,1738],{"class":89,"line":629},[87,1739,620],{"class":148},[87,1741,1742],{"class":89,"line":647},[87,1743,1062],{"class":148},[73,1745,1746],{"id":1746},"组合使用",[78,1748,1750],{"className":118,"code":1749,"language":94,"meta":83,"style":83},"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",[30,1751,1752,1764,1775,1786,1799],{"__ignoreMap":83},[87,1753,1754,1756,1758,1760,1762],{"class":89,"line":90},[87,1755,747],{"class":148},[87,1757,750],{"class":93},[87,1759,219],{"class":148},[87,1761,710],{"class":97},[87,1763,294],{"class":148},[87,1765,1766,1769,1772],{"class":89,"line":104},[87,1767,1768],{"class":93},"    AuthRequired",[87,1770,1771],{"class":148},"(authService),  ",[87,1773,1774],{"class":173},"\u002F\u002F 1. 验证 Token\n",[87,1776,1777,1780,1783],{"class":89,"line":145},[87,1778,1779],{"class":93},"    WithUserContext",[87,1781,1782],{"class":148},"(),           ",[87,1784,1785],{"class":173},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[87,1787,1788,1791,1793,1796],{"class":89,"line":164},[87,1789,1790],{"class":148},"    gin.",[87,1792,760],{"class":93},[87,1794,1795],{"class":148},"(mcpHTTP),         ",[87,1797,1798],{"class":173},"\u002F\u002F 3. MCP 处理\n",[87,1800,1801],{"class":89,"line":177},[87,1802,231],{"class":148},[16,1804,1805],{},"在工具处理器里就能拿到用户信息了：",[78,1807,1809],{"className":118,"code":1808,"language":94,"meta":83,"style":83},"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",[30,1810,1811,1867,1888,1897,1914,1918,1922,1938,1943,1956],{"__ignoreMap":83},[87,1812,1813,1815,1818,1821,1823,1826,1828,1831,1833,1835,1837,1839,1841,1843,1845,1847,1849,1851,1853,1855,1857,1859,1861,1863,1865],{"class":89,"line":90},[87,1814,336],{"class":126},[87,1816,1817],{"class":148}," (",[87,1819,1820],{"class":341},"h ",[87,1822,371],{"class":126},[87,1824,1825],{"class":93},"Handler",[87,1827,1208],{"class":148},[87,1829,1830],{"class":93},"handleListItems",[87,1832,219],{"class":148},[87,1834,342],{"class":341},[87,1836,345],{"class":93},[87,1838,348],{"class":148},[87,1840,351],{"class":93},[87,1842,354],{"class":148},[87,1844,357],{"class":341},[87,1846,360],{"class":93},[87,1848,348],{"class":148},[87,1850,365],{"class":93},[87,1852,368],{"class":148},[87,1854,371],{"class":126},[87,1856,374],{"class":93},[87,1858,348],{"class":148},[87,1860,379],{"class":93},[87,1862,354],{"class":148},[87,1864,384],{"class":126},[87,1866,387],{"class":148},[87,1868,1869,1872,1874,1877,1880,1883,1886],{"class":89,"line":104},[87,1870,1871],{"class":148},"    userID, ok ",[87,1873,152],{"class":126},[87,1875,1876],{"class":148}," ctx.",[87,1878,1879],{"class":93},"Value",[87,1881,1882],{"class":148},"(ctxUserID).(",[87,1884,1885],{"class":126},"uint",[87,1887,231],{"class":148},[87,1889,1890,1892,1894],{"class":89,"line":145},[87,1891,583],{"class":126},[87,1893,1368],{"class":126},[87,1895,1896],{"class":148},"ok {\n",[87,1898,1899,1901,1903,1905,1907,1910,1912],{"class":89,"line":164},[87,1900,600],{"class":126},[87,1902,283],{"class":148},[87,1904,605],{"class":93},[87,1906,219],{"class":148},[87,1908,1909],{"class":97},"\"未认证\"",[87,1911,412],{"class":148},[87,1913,416],{"class":415},[87,1915,1916],{"class":89,"line":177},[87,1917,620],{"class":148},[87,1919,1920],{"class":89,"line":189},[87,1921,142],{"emptyLinePlaceholder":141},[87,1923,1924,1927,1929,1932,1935],{"class":89,"line":201},[87,1925,1926],{"class":148},"    items, _ ",[87,1928,152],{"class":126},[87,1930,1931],{"class":148}," h.service.",[87,1933,1934],{"class":93},"ListByUser",[87,1936,1937],{"class":148},"(userID)\n",[87,1939,1940],{"class":89,"line":211},[87,1941,1942],{"class":173},"    \u002F\u002F ...\n",[87,1944,1945,1947,1949,1951,1954],{"class":89,"line":228},[87,1946,399],{"class":126},[87,1948,283],{"class":148},[87,1950,404],{"class":93},[87,1952,1953],{"class":148},"(result), ",[87,1955,416],{"class":415},[87,1957,1958],{"class":89,"line":390},[87,1959,1062],{"class":148},[73,1961,1962],{"id":1962},"客户端配置",[16,1964,1965],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[78,1967,1971],{"className":1968,"code":1969,"language":1970,"meta":83,"style":83},"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",[30,1972,1973,1978,1986,1992,2004,2011,2021,2026,2030,2035],{"__ignoreMap":83},[87,1974,1975],{"class":89,"line":90},[87,1976,1977],{"class":148},"{\n",[87,1979,1980,1983],{"class":89,"line":104},[87,1981,1982],{"class":415},"  \"mcpServers\"",[87,1984,1985],{"class":148},": {\n",[87,1987,1988,1990],{"class":89,"line":145},[87,1989,167],{"class":415},[87,1991,1985],{"class":148},[87,1993,1994,1997,1999,2002],{"class":89,"line":164},[87,1995,1996],{"class":415},"      \"url\"",[87,1998,1025],{"class":148},[87,2000,2001],{"class":97},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[87,2003,294],{"class":148},[87,2005,2006,2009],{"class":89,"line":177},[87,2007,2008],{"class":415},"      \"headers\"",[87,2010,1985],{"class":148},[87,2012,2013,2016,2018],{"class":89,"line":189},[87,2014,2015],{"class":415},"        \"Authorization\"",[87,2017,1025],{"class":148},[87,2019,2020],{"class":97},"\"Bearer your-token-here\"\n",[87,2022,2023],{"class":89,"line":201},[87,2024,2025],{"class":148},"      }\n",[87,2027,2028],{"class":89,"line":211},[87,2029,620],{"class":148},[87,2031,2032],{"class":89,"line":228},[87,2033,2034],{"class":148},"  }\n",[87,2036,2037],{"class":89,"line":390},[87,2038,1062],{"class":148},[11,2040,2041],{"id":2041},"总结",[2043,2044,2045,2058],"table",{},[2046,2047,2048],"thead",{},[2049,2050,2051,2055],"tr",{},[2052,2053,2054],"th",{},"步骤",[2052,2056,2057],{},"说明",[2059,2060,2061,2071,2083,2092,2101],"tbody",{},[2049,2062,2063,2068],{},[2064,2065,2066],"td",{},[30,2067,158],{},[2064,2069,2070],{},"创建 MCP 协议实例",[2049,2072,2073,2080],{},[2064,2074,2075,2077,2078],{},[30,2076,286],{}," + ",[30,2079,330],{},[2064,2081,2082],{},"定义并注册工具",[2049,2084,2085,2089],{},[2064,2086,2087],{},[30,2088,695],{},[2064,2090,2091],{},"转为 HTTP Handler",[2049,2093,2094,2098],{},[2064,2095,2096],{},[30,2097,663],{},[2064,2099,2100],{},"挂载到 Gin 路由",[2049,2102,2103,2111],{},[2064,2104,2105,2077,2108],{},[30,2106,2107],{},"AuthRequired",[30,2109,2110],{},"WithUserContext",[2064,2112,2113],{},"鉴权 + 用户信息桥接",[16,2115,2116,2117,2120,2121,1178],{},"核心就一句话：",[38,2118,2119],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[30,2122,1177],{},[2124,2125,2126],"style",{},"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":83,"searchDepth":104,"depth":145,"links":2128},[2129,2130,2131,2132,2139,2145],{"id":13,"depth":104,"text":14},{"id":24,"depth":104,"text":25},{"id":43,"depth":104,"text":43},{"id":71,"depth":104,"text":71,"children":2133},[2134,2135,2136,2137,2138],{"id":75,"depth":145,"text":76},{"id":114,"depth":145,"text":115},{"id":247,"depth":145,"text":248},{"id":652,"depth":145,"text":653},{"id":773,"depth":145,"text":774},{"id":1149,"depth":104,"text":1150,"children":2140},[2141,2142,2143,2144],{"id":1156,"depth":145,"text":1156},{"id":1170,"depth":145,"text":1171},{"id":1746,"depth":145,"text":1746},{"id":1962,"depth":145,"text":1962},{"id":2041,"depth":104,"text":2041},"介绍如何使用 mark3labs\u002Fmcp-go 与 Gin 框架集成，搭建一个带鉴权的 MCP（Model Context Protocol）服务器，让 AI Agent 能直接调用你的后端服务。","进阶",false,"md","\u002Fposts\u002Fgo-mcp-gin\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-mcp-gin","2026-06-01T00:50:00",{"title":5,"description":2146},"Go 工程实践","posts\u002Fgo-mcp-gin\u002Findex",[2158,2159,2160,2161,2162],"Go","MCP","Gin","AI Agent","后端","2026-06-01","5fZl1Vigs2lHb5fHPz-0YZFyFD38ws1heOouZUG1g6M",[2166,2962,3377,3599,4013,5186,5464,7191,7973,8952,10361,10921,12100,13636],{"id":2167,"title":2168,"author":6,"body":2169,"description":2950,"difficulty":2147,"draft":2148,"extension":2149,"image":2951,"meta":2952,"navigation":141,"path":2953,"pinned":2148,"published":2954,"seo":2955,"series":2956,"seriesOrder":90,"stem":2957,"tags":2958,"updated":2954,"__hash__":2961},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":8,"value":2170,"toc":2939},[2171,2174,2177,2180,2399,2402,2444,2447,2465,2468,2616,2619,2755,2758,2761,2819,2822,2936],[11,2172,2173],{"id":2173},"解决问题",[16,2175,2176],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[11,2178,2179],{"id":2179},"查阅资料",[78,2181,2183],{"className":80,"code":2182,"language":82,"meta":83,"style":83},"# 启动容器并挂载当前目录到 `\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",[30,2184,2185,2190,2227,2231,2236,2264,2280,2294,2299,2311,2333,2341,2345,2350,2360,2370,2374,2382,2386],{"__ignoreMap":83},[87,2186,2187],{"class":89,"line":90},[87,2188,2189],{"class":173},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[87,2191,2192,2195,2198,2201,2204,2207,2209,2212,2215,2218,2221,2224],{"class":89,"line":104},[87,2193,2194],{"class":93},"docker",[87,2196,2197],{"class":97}," run",[87,2199,2200],{"class":415}," -it",[87,2202,2203],{"class":415}," --rm",[87,2205,2206],{"class":415}," -v",[87,2208,130],{"class":97},[87,2210,2211],{"class":148},"$PWD",[87,2213,2214],{"class":97},"\":\u002Fapp",[87,2216,2217],{"class":415}," -w",[87,2219,2220],{"class":97}," \u002Fapp",[87,2222,2223],{"class":97}," ubuntu:20.04",[87,2225,2226],{"class":97}," bash\n",[87,2228,2229],{"class":89,"line":145},[87,2230,142],{"emptyLinePlaceholder":141},[87,2232,2233],{"class":89,"line":164},[87,2234,2235],{"class":173},"# 在容器内执行以下操作：\n",[87,2237,2238,2241,2244,2247,2249,2252,2255,2258,2261],{"class":89,"line":177},[87,2239,2240],{"class":93},"apt-get",[87,2242,2243],{"class":97}," update",[87,2245,2246],{"class":148}," && ",[87,2248,2240],{"class":93},[87,2250,2251],{"class":97}," install",[87,2253,2254],{"class":415}," -y",[87,2256,2257],{"class":97}," musl-tools",[87,2259,2260],{"class":97}," golang-go",[87,2262,2263],{"class":173},"  # 安装 musl 和 Go\n",[87,2265,2266,2269,2272,2274,2277],{"class":89,"line":189},[87,2267,2268],{"class":126},"export",[87,2270,2271],{"class":148}," CC",[87,2273,1671],{"class":126},[87,2275,2276],{"class":148},"musl-gcc  ",[87,2278,2279],{"class":173},"# 指定 musl 编译器\n",[87,2281,2282,2284,2287,2289,2291],{"class":89,"line":201},[87,2283,2268],{"class":126},[87,2285,2286],{"class":148}," CGO_ENABLED",[87,2288,1671],{"class":126},[87,2290,1447],{"class":415},[87,2292,2293],{"class":173},"  # 启用 CGO\n",[87,2295,2296],{"class":89,"line":211},[87,2297,2298],{"class":173},"# 设置代理\n",[87,2300,2301,2303,2306,2308],{"class":89,"line":228},[87,2302,94],{"class":93},[87,2304,2305],{"class":97}," env",[87,2307,2217],{"class":415},[87,2309,2310],{"class":97}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[87,2312,2313,2315,2318,2321,2324,2327,2330],{"class":89,"line":390},[87,2314,94],{"class":93},[87,2316,2317],{"class":97}," build",[87,2319,2320],{"class":415}," -ldflags",[87,2322,2323],{"class":97}," '-linkmode external -extldflags \"-static\"'",[87,2325,2326],{"class":415}," -o",[87,2328,2329],{"class":97}," myapp",[87,2331,2332],{"class":173},"  # 静态编译\n",[87,2334,2335,2338],{"class":89,"line":396},[87,2336,2337],{"class":415},"exit",[87,2339,2340],{"class":173},"  # 退出容器\n",[87,2342,2343],{"class":89,"line":419},[87,2344,142],{"emptyLinePlaceholder":141},[87,2346,2347],{"class":89,"line":617},[87,2348,2349],{"class":173},"# 验证结果（在宿主机执行）\n",[87,2351,2352,2355,2357],{"class":89,"line":623},[87,2353,2354],{"class":93},"file",[87,2356,2329],{"class":97},[87,2358,2359],{"class":173},"  # 应显示 \"statically linked\"\n",[87,2361,2362,2365,2367],{"class":89,"line":629},[87,2363,2364],{"class":93},"ldd",[87,2366,2329],{"class":97},[87,2368,2369],{"class":173},"   # 应显示 \"not a dynamic executable\"\n",[87,2371,2372],{"class":89,"line":647},[87,2373,142],{"emptyLinePlaceholder":141},[87,2375,2376,2379],{"class":89,"line":927},[87,2377,2378],{"class":93},"wget",[87,2380,2381],{"class":97}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[87,2383,2384],{"class":89,"line":932},[87,2385,142],{"emptyLinePlaceholder":141},[87,2387,2388,2391,2394,2397],{"class":89,"line":951},[87,2389,2390],{"class":93},"ENV",[87,2392,2393],{"class":97}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[87,2395,2396],{"class":148},"$PATH",[87,2398,136],{"class":97},[73,2400,2401],{"id":2401},"遇到问题",[48,2403,2404,2430],{},[51,2405,2406,2409,2410,2413,2416,2417,2420,2422,2423,2425],{},[30,2407,2408],{},"go mod tidy"," 时报错 ",[30,2411,2412],{},"tls: failed to verify certificate: x509: certificate",[2414,2415],"br",{},"解决：",[30,2418,2419],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[2414,2421],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[2414,2424],{},[2426,2427],"img",{"alt":2428,"src":2429},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[51,2431,2432,2433,2436,2437,2439,2440,2443],{},"直接安装 ",[30,2434,2435],{},"golang-go"," 其版本过低",[2414,2438],{},"解决: ",[30,2441,2442],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[11,2445,2446],{"id":2446},"最终方案",[48,2448,2449,2456,2459],{},[51,2450,2451,2452,2455],{},"下载 ",[30,2453,2454],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[51,2457,2458],{},"编写 Dockerfile",[51,2460,2461,2462],{},"执行 ",[30,2463,2464],{},"docker build -t go-build:v1 .",[16,2466,2467],{},"Dockerfile 内容：",[78,2469,2473],{"className":2470,"code":2471,"language":2472,"meta":83,"style":83},"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",[30,2474,2475,2483,2487,2498,2502,2510,2514,2524,2528,2536,2540,2545,2552,2556,2563,2568,2575,2579,2587,2591,2598,2602],{"__ignoreMap":83},[87,2476,2477,2480],{"class":89,"line":90},[87,2478,2479],{"class":126},"FROM",[87,2481,2482],{"class":148}," ubuntu:20.04\n",[87,2484,2485],{"class":89,"line":104},[87,2486,142],{"emptyLinePlaceholder":141},[87,2488,2489,2492,2495],{"class":89,"line":145},[87,2490,2491],{"class":126},"LABEL",[87,2493,2494],{"class":148}," org.opencontainers.image.authors=",[87,2496,2497],{"class":97},"\"sutong\"\n",[87,2499,2500],{"class":89,"line":164},[87,2501,142],{"emptyLinePlaceholder":141},[87,2503,2504,2507],{"class":89,"line":177},[87,2505,2506],{"class":126},"COPY",[87,2508,2509],{"class":148}," go\u002F \u002Fgo\u002F\n",[87,2511,2512],{"class":89,"line":189},[87,2513,142],{"emptyLinePlaceholder":141},[87,2515,2516,2518,2521],{"class":89,"line":201},[87,2517,2390],{"class":126},[87,2519,2520],{"class":148}," PATH=",[87,2522,2523],{"class":97},"\"\u002Fgo\u002Fbin:$PATH\"\n",[87,2525,2526],{"class":89,"line":211},[87,2527,142],{"emptyLinePlaceholder":141},[87,2529,2530,2533],{"class":89,"line":228},[87,2531,2532],{"class":126},"RUN",[87,2534,2535],{"class":148}," apt-get update && apt-get install -y musl-tools\n",[87,2537,2538],{"class":89,"line":390},[87,2539,142],{"emptyLinePlaceholder":141},[87,2541,2542],{"class":89,"line":396},[87,2543,2544],{"class":173},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[87,2546,2547,2549],{"class":89,"line":419},[87,2548,2532],{"class":126},[87,2550,2551],{"class":148}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[87,2553,2554],{"class":89,"line":617},[87,2555,2279],{"class":173},[87,2557,2558,2560],{"class":89,"line":623},[87,2559,2390],{"class":126},[87,2561,2562],{"class":148}," CC=musl-gcc\n",[87,2564,2565],{"class":89,"line":629},[87,2566,2567],{"class":173},"# 启用 CGO\n",[87,2569,2570,2572],{"class":89,"line":647},[87,2571,2390],{"class":126},[87,2573,2574],{"class":148}," CGO_ENABLED=1\n",[87,2576,2577],{"class":89,"line":927},[87,2578,142],{"emptyLinePlaceholder":141},[87,2580,2581,2584],{"class":89,"line":932},[87,2582,2583],{"class":126},"VOLUME",[87,2585,2586],{"class":148}," \u002Fdata\n",[87,2588,2589],{"class":89,"line":951},[87,2590,142],{"emptyLinePlaceholder":141},[87,2592,2593,2596],{"class":89,"line":956},[87,2594,2595],{"class":126},"WORKDIR",[87,2597,2586],{"class":148},[87,2599,2600],{"class":89,"line":962},[87,2601,142],{"emptyLinePlaceholder":141},[87,2603,2604,2607,2610,2613],{"class":89,"line":996},[87,2605,2606],{"class":126},"CMD",[87,2608,2609],{"class":148}," [",[87,2611,2612],{"class":97},"\"\u002Fbin\u002Fbash\"",[87,2614,2615],{"class":148},"]\n",[73,2617,2618],{"id":2618},"构建",[78,2620,2622],{"className":80,"code":2621,"language":82,"meta":83,"style":83},"# 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",[30,2623,2624,2629,2633,2638,2664,2668,2673,2708,2713,2717,2750],{"__ignoreMap":83},[87,2625,2626],{"class":89,"line":90},[87,2627,2628],{"class":173},"# docker build -t go-build:v1 .\n",[87,2630,2631],{"class":89,"line":104},[87,2632,142],{"emptyLinePlaceholder":141},[87,2634,2635],{"class":89,"line":145},[87,2636,2637],{"class":173},"# 直接执行编译 编译后删除\n",[87,2639,2640,2642,2644,2646,2648,2651,2654,2657,2659,2661],{"class":89,"line":164},[87,2641,2194],{"class":93},[87,2643,2197],{"class":97},[87,2645,2203],{"class":415},[87,2647,2206],{"class":415},[87,2649,2650],{"class":97}," .:\u002Fdata",[87,2652,2653],{"class":97}," go-build:v1",[87,2655,2656],{"class":97}," go",[87,2658,2317],{"class":97},[87,2660,2326],{"class":415},[87,2662,2663],{"class":97}," app_musl\n",[87,2665,2666],{"class":89,"line":177},[87,2667,142],{"emptyLinePlaceholder":141},[87,2669,2670],{"class":89,"line":189},[87,2671,2672],{"class":173},"# 构建编译环境\n",[87,2674,2675,2677,2679,2681,2684,2686,2688,2691,2694,2697,2700,2703,2705],{"class":89,"line":201},[87,2676,2194],{"class":93},[87,2678,2197],{"class":97},[87,2680,2200],{"class":415},[87,2682,2683],{"class":415}," -d",[87,2685,2206],{"class":415},[87,2687,2650],{"class":97},[87,2689,2690],{"class":415}," --name",[87,2692,2693],{"class":126}," \u003C",[87,2695,2696],{"class":97},"容器名",[87,2698,2699],{"class":148},"称",[87,2701,2702],{"class":126},">",[87,2704,2653],{"class":97},[87,2706,2707],{"class":97}," \u002Fbin\u002Fbash\n",[87,2709,2710],{"class":89,"line":211},[87,2711,2712],{"class":173},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[87,2714,2715],{"class":89,"line":228},[87,2716,142],{"emptyLinePlaceholder":141},[87,2718,2719,2721,2724,2726,2728,2730,2732,2734,2736,2738,2740,2742,2745,2747],{"class":89,"line":390},[87,2720,2194],{"class":93},[87,2722,2723],{"class":97}," exec",[87,2725,2200],{"class":415},[87,2727,2693],{"class":126},[87,2729,2696],{"class":97},[87,2731,2699],{"class":148},[87,2733,2702],{"class":126},[87,2735,2656],{"class":97},[87,2737,2317],{"class":97},[87,2739,2326],{"class":415},[87,2741,2693],{"class":126},[87,2743,2744],{"class":97},"myap",[87,2746,16],{"class":148},[87,2748,2749],{"class":126},">\n",[87,2751,2752],{"class":89,"line":396},[87,2753,2754],{"class":173},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[73,2756,2757],{"id":2757},"额外注意",[16,2759,2760],{},"若没科学上网需要设置代理：",[78,2762,2764],{"className":80,"code":2763,"language":82,"meta":83,"style":83},"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",[30,2765,2766,2784,2794,2810],{"__ignoreMap":83},[87,2767,2768,2770,2772,2774,2776,2778,2780,2782],{"class":89,"line":90},[87,2769,2194],{"class":93},[87,2771,2723],{"class":97},[87,2773,2200],{"class":415},[87,2775,2693],{"class":126},[87,2777,2696],{"class":97},[87,2779,2699],{"class":148},[87,2781,2702],{"class":126},[87,2783,2707],{"class":97},[87,2785,2786,2788,2790,2792],{"class":89,"line":104},[87,2787,94],{"class":93},[87,2789,2305],{"class":97},[87,2791,2217],{"class":415},[87,2793,2310],{"class":97},[87,2795,2796,2798,2800,2802,2804,2806,2808],{"class":89,"line":145},[87,2797,94],{"class":93},[87,2799,2317],{"class":97},[87,2801,2320],{"class":415},[87,2803,2323],{"class":97},[87,2805,2326],{"class":415},[87,2807,2329],{"class":97},[87,2809,2332],{"class":173},[87,2811,2812,2814,2816],{"class":89,"line":164},[87,2813,94],{"class":93},[87,2815,2317],{"class":97},[87,2817,2818],{"class":173}," # musl 库编译\n",[73,2820,2821],{"id":2821},"运行环境示例",[78,2823,2825],{"className":2470,"code":2824,"language":2472,"meta":83,"style":83},"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",[30,2826,2827,2834,2838,2846,2850,2857,2861,2868,2875,2879,2886,2890,2897,2901,2922,2926,2931],{"__ignoreMap":83},[87,2828,2829,2831],{"class":89,"line":90},[87,2830,2479],{"class":126},[87,2832,2833],{"class":148}," alpine:latest\n",[87,2835,2836],{"class":89,"line":104},[87,2837,142],{"emptyLinePlaceholder":141},[87,2839,2840,2842,2844],{"class":89,"line":145},[87,2841,2491],{"class":126},[87,2843,2494],{"class":148},[87,2845,2497],{"class":97},[87,2847,2848],{"class":89,"line":164},[87,2849,142],{"emptyLinePlaceholder":141},[87,2851,2852,2854],{"class":89,"line":177},[87,2853,2506],{"class":126},[87,2855,2856],{"class":148}," glog_musl \u002Fapp\u002F\n",[87,2858,2859],{"class":89,"line":189},[87,2860,142],{"emptyLinePlaceholder":141},[87,2862,2863,2865],{"class":89,"line":201},[87,2864,2506],{"class":126},[87,2866,2867],{"class":148}," conf.ini \u002Fdata\u002F\n",[87,2869,2870,2872],{"class":89,"line":211},[87,2871,2506],{"class":126},[87,2873,2874],{"class":148}," ipname.ini \u002Fdata\u002F\n",[87,2876,2877],{"class":89,"line":228},[87,2878,142],{"emptyLinePlaceholder":141},[87,2880,2881,2883],{"class":89,"line":390},[87,2882,2595],{"class":126},[87,2884,2885],{"class":148}," \u002Fapp\n",[87,2887,2888],{"class":89,"line":396},[87,2889,142],{"emptyLinePlaceholder":141},[87,2891,2892,2894],{"class":89,"line":419},[87,2893,2532],{"class":126},[87,2895,2896],{"class":148}," chmod 777 \u002Fapp\u002Fglog_musl\n",[87,2898,2899],{"class":89,"line":617},[87,2900,142],{"emptyLinePlaceholder":141},[87,2902,2903,2905,2907,2910,2912,2915,2917,2920],{"class":89,"line":623},[87,2904,2606],{"class":126},[87,2906,2609],{"class":148},[87,2908,2909],{"class":97},"\".\u002Fglog_musl\"",[87,2911,354],{"class":148},[87,2913,2914],{"class":97},"\"logsweb\"",[87,2916,354],{"class":148},[87,2918,2919],{"class":97},"\"\u002Fdata\u002Fconf.ini\"",[87,2921,2615],{"class":148},[87,2923,2924],{"class":89,"line":629},[87,2925,142],{"emptyLinePlaceholder":141},[87,2927,2928],{"class":89,"line":647},[87,2929,2930],{"class":173},"# docker build -t glog:v1 .\n",[87,2932,2933],{"class":89,"line":927},[87,2934,2935],{"class":173},"# docker run -p 6801:6801 glog:v1\n",[2124,2937,2938],{},"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":83,"searchDepth":104,"depth":145,"links":2940},[2941,2942,2945],{"id":2173,"depth":104,"text":2173},{"id":2179,"depth":104,"text":2179,"children":2943},[2944],{"id":2401,"depth":145,"text":2401},{"id":2446,"depth":104,"text":2446,"children":2946},[2947,2948,2949],{"id":2618,"depth":145,"text":2618},{"id":2757,"depth":145,"text":2757},{"id":2821,"depth":145,"text":2821},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":2168,"description":2950},"Docker 实践笔记","posts\u002Fdocker-musl-build\u002Findex",[2959,2158,2960],"Docker","运维","YLtdOOc-gMVDSNjE-ntKXz-7hff-VtvMB2dB5RXNi1o",{"id":2963,"title":2964,"author":6,"body":2965,"description":3366,"difficulty":3367,"draft":2148,"extension":2149,"image":3368,"meta":3369,"navigation":141,"path":3370,"pinned":2148,"published":3371,"seo":3372,"series":2956,"seriesOrder":104,"stem":3373,"tags":3374,"updated":3371,"__hash__":3376},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":8,"value":2966,"toc":3356},[2967,2970,2973,2976,2980,2983,3000,3004,3037,3041,3048,3098,3101,3316,3318,3321,3350,3353],[11,2968,2969],{"id":2969},"前言",[16,2971,2972],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[11,2974,2975],{"id":2975},"解决方案",[73,2977,2979],{"id":2978},"_1-安装用户管理工具","1. 安装用户管理工具",[16,2981,2982],{},"若镜像非自带用户管理：",[78,2984,2986],{"className":2470,"code":2985,"language":2472,"meta":83,"style":83},"# 权限管理\nRUN apk add --no-cache shadow\n",[30,2987,2988,2993],{"__ignoreMap":83},[87,2989,2990],{"class":89,"line":90},[87,2991,2992],{"class":173},"# 权限管理\n",[87,2994,2995,2997],{"class":89,"line":104},[87,2996,2532],{"class":126},[87,2998,2999],{"class":148}," apk add --no-cache shadow\n",[73,3001,3003],{"id":3002},"_2-创建用户并修改权限","2. 创建用户并修改权限",[78,3005,3007],{"className":2470,"code":3006,"language":2472,"meta":83,"style":83},"# 创建用户组和用户\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",[30,3008,3009,3014,3021,3025,3030],{"__ignoreMap":83},[87,3010,3011],{"class":89,"line":90},[87,3012,3013],{"class":173},"# 创建用户组和用户\n",[87,3015,3016,3018],{"class":89,"line":104},[87,3017,2532],{"class":126},[87,3019,3020],{"class":148}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[87,3022,3023],{"class":89,"line":145},[87,3024,142],{"emptyLinePlaceholder":141},[87,3026,3027],{"class":89,"line":164},[87,3028,3029],{"class":173},"# 修改目录权限\n",[87,3031,3032,3034],{"class":89,"line":177},[87,3033,2532],{"class":126},[87,3035,3036],{"class":148}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[73,3038,3040],{"id":3039},"_3-指定用户启动","3. 指定用户启动",[16,3042,3043,3044,3047],{},"执行 docker 时加 ",[30,3045,3046],{},"--user"," 参数：",[78,3049,3051],{"className":80,"code":3050,"language":82,"meta":83,"style":83},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[30,3052,3053],{"__ignoreMap":83},[87,3054,3055,3057,3059,3061,3064,3066,3069,3072,3074,3076,3079,3082,3084,3087,3090,3093,3096],{"class":89,"line":90},[87,3056,2194],{"class":93},[87,3058,2197],{"class":97},[87,3060,2200],{"class":415},[87,3062,3063],{"class":415}," --user",[87,3065,2693],{"class":126},[87,3067,3068],{"class":97},"app_use",[87,3070,3071],{"class":148},"r",[87,3073,2702],{"class":126},[87,3075,2693],{"class":126},[87,3077,3078],{"class":97},"app_imag",[87,3080,3081],{"class":148},"e",[87,3083,2702],{"class":126},[87,3085,3086],{"class":97},":",[87,3088,3089],{"class":126},"\u003C",[87,3091,3092],{"class":97},"image_versio",[87,3094,3095],{"class":148},"n",[87,3097,2749],{"class":126},[11,3099,3100],{"id":3100},"完整示例",[78,3102,3104],{"className":2470,"code":3103,"language":2472,"meta":83,"style":83},"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",[30,3105,3106,3112,3116,3121,3134,3138,3145,3149,3155,3160,3167,3172,3179,3183,3190,3194,3198,3205,3209,3216,3220,3227,3231,3244,3250,3254,3259,3267,3275,3280,3286,3295],{"__ignoreMap":83},[87,3107,3108,3110],{"class":89,"line":90},[87,3109,2479],{"class":126},[87,3111,2833],{"class":148},[87,3113,3114],{"class":89,"line":104},[87,3115,142],{"emptyLinePlaceholder":141},[87,3117,3118],{"class":89,"line":145},[87,3119,3120],{"class":173},"# 国内源\n",[87,3122,3123,3125,3128,3131],{"class":89,"line":164},[87,3124,2532],{"class":126},[87,3126,3127],{"class":148}," sed -i ",[87,3129,3130],{"class":97},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[87,3132,3133],{"class":148}," \u002Fetc\u002Fapk\u002Frepositories\n",[87,3135,3136],{"class":89,"line":177},[87,3137,142],{"emptyLinePlaceholder":141},[87,3139,3140,3142],{"class":89,"line":189},[87,3141,2532],{"class":126},[87,3143,3144],{"class":148}," apk update --no-cache\n",[87,3146,3147],{"class":89,"line":201},[87,3148,2992],{"class":173},[87,3150,3151,3153],{"class":89,"line":211},[87,3152,2532],{"class":126},[87,3154,2999],{"class":148},[87,3156,3157],{"class":89,"line":228},[87,3158,3159],{"class":173},"# 调试\n",[87,3161,3162,3164],{"class":89,"line":390},[87,3163,2532],{"class":126},[87,3165,3166],{"class":148}," apk add --no-cache bash\n",[87,3168,3169],{"class":89,"line":396},[87,3170,3171],{"class":173},"# 设置时区\n",[87,3173,3174,3176],{"class":89,"line":419},[87,3175,2532],{"class":126},[87,3177,3178],{"class":148}," apk add --no-cache tzdata\n",[87,3180,3181],{"class":89,"line":617},[87,3182,142],{"emptyLinePlaceholder":141},[87,3184,3185,3187],{"class":89,"line":623},[87,3186,2390],{"class":126},[87,3188,3189],{"class":148}," TZ=Asia\u002FShanghai\n",[87,3191,3192],{"class":89,"line":629},[87,3193,142],{"emptyLinePlaceholder":141},[87,3195,3196],{"class":89,"line":647},[87,3197,3013],{"class":173},[87,3199,3200,3202],{"class":89,"line":927},[87,3201,2532],{"class":126},[87,3203,3204],{"class":148}," addgroup -S glog && adduser -S -G glog glog\n",[87,3206,3207],{"class":89,"line":932},[87,3208,142],{"emptyLinePlaceholder":141},[87,3210,3211,3213],{"class":89,"line":951},[87,3212,2506],{"class":126},[87,3214,3215],{"class":148}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[87,3217,3218],{"class":89,"line":956},[87,3219,142],{"emptyLinePlaceholder":141},[87,3221,3222,3224],{"class":89,"line":962},[87,3223,2532],{"class":126},[87,3225,3226],{"class":148}," mkdir \u002Fdata\n",[87,3228,3229],{"class":89,"line":996},[87,3230,142],{"emptyLinePlaceholder":141},[87,3232,3233,3235,3238,3241],{"class":89,"line":1033},[87,3234,2583],{"class":126},[87,3236,3237],{"class":148}," [ ",[87,3239,3240],{"class":97},"\"\u002Fdata\"",[87,3242,3243],{"class":148}," ]\n",[87,3245,3246,3248],{"class":89,"line":1039},[87,3247,2595],{"class":126},[87,3249,2586],{"class":148},[87,3251,3252],{"class":89,"line":1044},[87,3253,142],{"emptyLinePlaceholder":141},[87,3255,3256],{"class":89,"line":1059},[87,3257,3258],{"class":173},"# 修改权限\n",[87,3260,3262,3264],{"class":89,"line":3261},27,[87,3263,2532],{"class":126},[87,3265,3266],{"class":148}," chown -R glog:glog \u002Fdata\n",[87,3268,3270,3272],{"class":89,"line":3269},28,[87,3271,2532],{"class":126},[87,3273,3274],{"class":148}," chown -R glog:glog \u002Fapp\n",[87,3276,3278],{"class":89,"line":3277},29,[87,3279,142],{"emptyLinePlaceholder":141},[87,3281,3283],{"class":89,"line":3282},30,[87,3284,3285],{"class":173},"# 指定用户启动\n",[87,3287,3289,3292],{"class":89,"line":3288},31,[87,3290,3291],{"class":126},"USER",[87,3293,3294],{"class":148}," glog\n",[87,3296,3298,3300,3302,3305,3307,3309,3311,3314],{"class":89,"line":3297},32,[87,3299,2606],{"class":126},[87,3301,3237],{"class":148},[87,3303,3304],{"class":97},"\"\u002Fapp\u002Fglog_static_musl_1\"",[87,3306,354],{"class":148},[87,3308,2914],{"class":97},[87,3310,354],{"class":148},[87,3312,3313],{"class":97},"\".\u002Fconf.ini\"",[87,3315,3243],{"class":148},[11,3317,2041],{"id":2041},[16,3319,3320],{},"关键步骤：",[48,3322,3323,3329,3338,3344],{},[51,3324,3325,3328],{},[30,3326,3327],{},"apk add --no-cache shadow"," - 安装用户管理工具",[51,3330,3331,2077,3334,3337],{},[30,3332,3333],{},"addgroup",[30,3335,3336],{},"adduser"," - 创建用户",[51,3339,3340,3343],{},[30,3341,3342],{},"chown -R"," - 修改目录权限",[51,3345,3346,3349],{},[30,3347,3348],{},"USER \u003Cusername>"," - 指定运行用户",[16,3351,3352],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[2124,3354,3355],{},"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":83,"searchDepth":104,"depth":145,"links":3357},[3358,3359,3364,3365],{"id":2969,"depth":104,"text":2969},{"id":2975,"depth":104,"text":2975,"children":3360},[3361,3362,3363],{"id":2978,"depth":145,"text":2979},{"id":3002,"depth":145,"text":3003},{"id":3039,"depth":145,"text":3040},{"id":3100,"depth":104,"text":3100},{"id":2041,"depth":104,"text":2041},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","入门","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":2964,"description":3366},"posts\u002Fdocker-user-start\u002Findex",[2959,2960,3375],"Linux","b4Uyw5ms87xr7NhpIkavHnNbT20oaEPtXz8k1w6UPj0",{"id":3378,"title":3379,"author":6,"body":3380,"description":3589,"difficulty":6,"draft":2148,"extension":2149,"image":3590,"meta":3591,"navigation":141,"path":3592,"pinned":2148,"published":3593,"seo":3594,"series":6,"seriesOrder":6,"stem":3595,"tags":3596,"updated":6,"__hash__":3598},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":8,"value":3381,"toc":3579},[3382,3386,3389,3395,3398,3402,3409,3459,3463,3469,3477,3481,3484,3487,3557,3559,3570,3576],[11,3383,3385],{"id":3384},"什么是-sveltekit","什么是 SvelteKit？",[16,3387,3388],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[16,3390,3391],{},[2426,3392],{"alt":3393,"src":3394},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[11,3396,3397],{"id":3397},"核心特点",[73,3399,3401],{"id":3400},"_1-编译时框架","1. 编译时框架",[16,3403,3404,3405,3408],{},"Svelte 和 React\u002FVue 最大的区别在于：",[38,3406,3407],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[78,3410,3414],{"className":3411,"code":3412,"language":3413,"meta":83,"style":83},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n","typescript",[30,3415,3416,3421,3434,3444,3455],{"__ignoreMap":83},[87,3417,3418],{"class":89,"line":90},[87,3419,3420],{"class":173},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[87,3422,3423,3426,3429,3431],{"class":89,"line":104},[87,3424,3425],{"class":126},"let",[87,3427,3428],{"class":148}," count ",[87,3430,1671],{"class":126},[87,3432,3433],{"class":415}," 0\n",[87,3435,3436,3439,3442],{"class":89,"line":145},[87,3437,3438],{"class":126},"function",[87,3440,3441],{"class":93}," increment",[87,3443,789],{"class":148},[87,3445,3446,3449,3452],{"class":89,"line":164},[87,3447,3448],{"class":148},"  count ",[87,3450,3451],{"class":126},"+=",[87,3453,3454],{"class":415}," 1\n",[87,3456,3457],{"class":89,"line":177},[87,3458,1062],{"class":148},[73,3460,3462],{"id":3461},"_2-文件路由","2. 文件路由",[16,3464,3465,3468],{},[30,3466,3467],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[78,3470,3475],{"className":3471,"code":3473,"language":3474},[3472],"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",[30,3476,3473],{"__ignoreMap":83},[73,3478,3480],{"id":3479},"_3-全栈能力","3. 全栈能力",[16,3482,3483],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[11,3485,3486],{"id":3486},"和其他框架对比",[2043,3488,3489,3505],{},[2046,3490,3491],{},[2049,3492,3493,3496,3499,3502],{},[2052,3494,3495],{},"特性",[2052,3497,3498],{},"SvelteKit",[2052,3500,3501],{},"Next.js",[2052,3503,3504],{},"Nuxt",[2059,3506,3507,3520,3533,3546],{},[2049,3508,3509,3512,3515,3518],{},[2064,3510,3511],{},"学习曲线",[2064,3513,3514],{},"低",[2064,3516,3517],{},"中",[2064,3519,3517],{},[2049,3521,3522,3525,3528,3531],{},[2064,3523,3524],{},"包体积",[2064,3526,3527],{},"小",[2064,3529,3530],{},"大",[2064,3532,3517],{},[2049,3534,3535,3538,3541,3544],{},[2064,3536,3537],{},"运行时开销",[2064,3539,3540],{},"无",[2064,3542,3543],{},"有",[2064,3545,3543],{},[2049,3547,3548,3551,3553,3555],{},[2064,3549,3550],{},"生态",[2064,3552,3527],{},[2064,3554,3530],{},[2064,3556,3530],{},[11,3558,2041],{"id":2041},[16,3560,3561,3562,3565,3566,3569],{},"如果你追求",[38,3563,3564],{},"简洁的代码","和",[38,3567,3568],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[16,3571,3572],{},[2426,3573],{"alt":3574,"src":3575},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[2124,3577,3578],{},"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":83,"searchDepth":104,"depth":145,"links":3580},[3581,3582,3587,3588],{"id":3384,"depth":104,"text":3385},{"id":3397,"depth":104,"text":3397,"children":3583},[3584,3585,3586],{"id":3400,"depth":145,"text":3401},{"id":3461,"depth":145,"text":3462},{"id":3479,"depth":145,"text":3480},{"id":3486,"depth":104,"text":3486},{"id":2041,"depth":104,"text":2041},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":3379,"description":3589},"posts\u002Fsveltekit-intro\u002Findex",[3498,3597],"前端框架","vsEGyunpCWR7DiJAdyV1SPDfa57xXLITBI-lKM8UeGQ",{"id":3600,"title":3601,"author":6,"body":3602,"description":4001,"difficulty":3367,"draft":2148,"extension":2149,"image":4002,"meta":4003,"navigation":141,"path":4004,"pinned":141,"published":4005,"seo":4006,"series":4007,"seriesOrder":90,"stem":4008,"tags":4009,"updated":4011,"__hash__":4012},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎使用 SuBlog",{"type":8,"value":3603,"toc":3992},[3604,3608,3615,3618,3621,3624,3698,3701,3704,3767,3770,3773,3836,3839,3904,3907,3910,3926,3933,3936,3967,3970,3981,3984,3989],[11,3605,3607],{"id":3606},"博客上线啦","博客上线啦！",[16,3609,3610,3611,3614],{},"Hello World！欢迎使用 ",[38,3612,3613],{},"SuBlog","，这是站点的第一篇示例文章。",[16,3616,3617],{},"经过一段时间的折腾，这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。",[11,3619,3620],{"id":3620},"技术栈",[16,3622,3623],{},"本站使用了以下技术：",[2043,3625,3626,3636],{},[2046,3627,3628],{},[2049,3629,3630,3633],{},[2052,3631,3632],{},"技术",[2052,3634,3635],{},"用途",[2059,3637,3638,3648,3658,3668,3678,3688],{},[2049,3639,3640,3645],{},[2064,3641,3642],{},[38,3643,3644],{},"Nuxt 3",[2064,3646,3647],{},"Vue 3 全栈框架，提供 SSG、路由、API 等能力",[2049,3649,3650,3655],{},[2064,3651,3652],{},[38,3653,3654],{},"TypeScript",[2064,3656,3657],{},"类型安全，提升开发体验",[2049,3659,3660,3665],{},[2064,3661,3662],{},[38,3663,3664],{},"Tailwind CSS v4",[2064,3666,3667],{},"原子化 CSS，快速构建 UI",[2049,3669,3670,3675],{},[2064,3671,3672],{},[38,3673,3674],{},"@nuxt\u002Fcontent",[2064,3676,3677],{},"Markdown 驱动的内容管理",[2049,3679,3680,3685],{},[2064,3681,3682],{},[38,3683,3684],{},"Shiki",[2064,3686,3687],{},"代码语法高亮",[2049,3689,3690,3695],{},[2064,3691,3692],{},[38,3693,3694],{},"Giscus",[2064,3696,3697],{},"基于 GitHub Discussions 的评论系统",[11,3699,3700],{"id":3700},"功能特性",[16,3702,3703],{},"目前已实现的功能：",[3705,3706,3707,3713,3719,3725,3731,3737,3743,3749,3755,3761],"ul",{},[51,3708,3709,3712],{},[38,3710,3711],{},"博客系统"," - Markdown 文章，支持代码高亮、目录导航、搜索",[51,3714,3715,3718],{},[38,3716,3717],{},"暗色模式"," - 跟随系统或手动切换",[51,3720,3721,3724],{},[38,3722,3723],{},"文章搜索"," - 支持标题、描述、正文、标签多维度搜索",[51,3726,3727,3730],{},[38,3728,3729],{},"标签系统"," - 文章标签分类，高频标签快速筛选",[51,3732,3733,3736],{},[38,3734,3735],{},"友链页面"," - 展示友情链接，支持申请",[51,3738,3739,3742],{},[38,3740,3741],{},"赞助页面"," - 微信收款码展示",[51,3744,3745,3748],{},[38,3746,3747],{},"封面制作"," - 在线生成封面图片工具",[51,3750,3751,3754],{},[38,3752,3753],{},"网易云热评"," - 首页随机展示音乐评论",[51,3756,3757,3760],{},[38,3758,3759],{},"访客统计"," - 不蒜子计数器，带数字滚动动画",[51,3762,3763,3766],{},[38,3764,3765],{},"SEO 优化"," - 自动生成站点地图、Open Graph 标签",[11,3768,3769],{"id":3769},"代码示例",[16,3771,3772],{},"本站的配置非常集中，一个文件搞定全站定制：",[78,3774,3776],{"className":3411,"code":3775,"language":3413,"meta":83,"style":83},"\u002F\u002F config\u002Fsite.ts\nexport const siteConfig = {\n  siteName: '我的博客',\n  url: 'https:\u002F\u002Fexample.com\u002F',\n  avatar: 'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100',\n  \u002F\u002F ... 改这一个文件，全站生效\n}\n",[30,3777,3778,3783,3797,3807,3817,3827,3832],{"__ignoreMap":83},[87,3779,3780],{"class":89,"line":90},[87,3781,3782],{"class":173},"\u002F\u002F config\u002Fsite.ts\n",[87,3784,3785,3787,3790,3793,3795],{"class":89,"line":104},[87,3786,2268],{"class":126},[87,3788,3789],{"class":126}," const",[87,3791,3792],{"class":415}," siteConfig",[87,3794,1589],{"class":126},[87,3796,595],{"class":148},[87,3798,3799,3802,3805],{"class":89,"line":145},[87,3800,3801],{"class":148},"  siteName: ",[87,3803,3804],{"class":97},"'我的博客'",[87,3806,294],{"class":148},[87,3808,3809,3812,3815],{"class":89,"line":164},[87,3810,3811],{"class":148},"  url: ",[87,3813,3814],{"class":97},"'https:\u002F\u002Fexample.com\u002F'",[87,3816,294],{"class":148},[87,3818,3819,3822,3825],{"class":89,"line":177},[87,3820,3821],{"class":148},"  avatar: ",[87,3823,3824],{"class":97},"'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100'",[87,3826,294],{"class":148},[87,3828,3829],{"class":89,"line":189},[87,3830,3831],{"class":173},"  \u002F\u002F ... 改这一个文件，全站生效\n",[87,3833,3834],{"class":89,"line":201},[87,3835,1062],{"class":148},[16,3837,3838],{},"文章的 frontmatter 也很简洁：",[78,3840,3844],{"className":3841,"code":3842,"language":3843,"meta":83,"style":83},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 文章标题\npublished: 2026-05-31T08:00:00\ndescription: 文章简介\ntags: ['Nuxt', 'TypeScript']\n---\n","yaml",[30,3845,3846,3851,3862,3872,3882,3900],{"__ignoreMap":83},[87,3847,3848],{"class":89,"line":90},[87,3849,3850],{"class":93},"---\n",[87,3852,3853,3857,3859],{"class":89,"line":104},[87,3854,3856],{"class":3855},"sbB4o","title",[87,3858,1025],{"class":148},[87,3860,3861],{"class":97},"文章标题\n",[87,3863,3864,3867,3869],{"class":89,"line":145},[87,3865,3866],{"class":3855},"published",[87,3868,1025],{"class":148},[87,3870,3871],{"class":415},"2026-05-31T08:00:00\n",[87,3873,3874,3877,3879],{"class":89,"line":164},[87,3875,3876],{"class":3855},"description",[87,3878,1025],{"class":148},[87,3880,3881],{"class":97},"文章简介\n",[87,3883,3884,3887,3890,3893,3895,3898],{"class":89,"line":177},[87,3885,3886],{"class":3855},"tags",[87,3888,3889],{"class":148},": [",[87,3891,3892],{"class":97},"'Nuxt'",[87,3894,354],{"class":148},[87,3896,3897],{"class":97},"'TypeScript'",[87,3899,2615],{"class":148},[87,3901,3902],{"class":89,"line":189},[87,3903,3850],{"class":93},[11,3905,3906],{"id":3906},"部署",[16,3908,3909],{},"本站使用 SSG 静态生成，构建后直接部署到静态托管即可：",[78,3911,3913],{"className":80,"code":3912,"language":82,"meta":83,"style":83},"npx nuxi generate\n",[30,3914,3915],{"__ignoreMap":83},[87,3916,3917,3920,3923],{"class":89,"line":90},[87,3918,3919],{"class":93},"npx",[87,3921,3922],{"class":97}," nuxi",[87,3924,3925],{"class":97}," generate\n",[16,3927,3928,3929,3932],{},"构建产物在 ",[30,3930,3931],{},".output\u002Fpublic"," 目录下，可以直接丢到 Nginx、Vercel、Netlify 等平台。",[11,3934,3935],{"id":3935},"后续计划",[3705,3937,3940,3949,3955,3961],{"className":3938},[3939],"contains-task-list",[51,3941,3944,3948],{"className":3942},[3943],"task-list-item",[3945,3946],"input",{"disabled":141,"type":3947},"checkbox"," RSS 订阅",[51,3950,3952,3954],{"className":3951},[3943],[3945,3953],{"disabled":141,"type":3947}," 文章目录优化",[51,3956,3958,3960],{"className":3957},[3943],[3945,3959],{"disabled":141,"type":3947}," 更多小工具",[51,3962,3964,3966],{"className":3963},[3943],[3945,3965],{"disabled":141,"type":3947}," 持续输出内容",[11,3968,3969],{"id":3969},"致谢",[16,3971,3972,3973,3980],{},"感谢 ",[3974,3975,3979],"a",{"href":3976,"rel":3977},"https:\u002F\u002F2x.nz",[3978],"nofollow","二叉树树"," 的开源项目，本站的 UI 设计参考了他的项目。",[3982,3983],"hr",{},[16,3985,3986],{},[38,3987,3988],{},"感谢阅读！如果觉得不错，欢迎点个 Star 或者留个评论 (☞ﾟヮﾟ)☞",[2124,3990,3991],{},"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":83,"searchDepth":104,"depth":145,"links":3993},[3994,3995,3996,3997,3998,3999,4000],{"id":3606,"depth":104,"text":3607},{"id":3620,"depth":104,"text":3620},{"id":3700,"depth":104,"text":3700},{"id":3769,"depth":104,"text":3769},{"id":3906,"depth":104,"text":3906},{"id":3935,"depth":104,"text":3935},{"id":3969,"depth":104,"text":3969},"博客上线啦！这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。","\u002Fposts\u002Fhello-world\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fhello-world","2026-05-31T08:00:00",{"title":3601,"description":4001},"Nuxt 博客开发","posts\u002Fhello-world\u002Findex",[4010,3504],"博客","2026-05-31","f9MCboCBfQIUxT8aKv6aKGDFxysiG8ZG0YXr5X5LTFo",{"id":4014,"title":4015,"author":6,"body":4016,"description":5175,"difficulty":2147,"draft":2148,"extension":2149,"image":5176,"meta":5177,"navigation":141,"path":5178,"pinned":2148,"published":5179,"seo":5180,"series":4007,"seriesOrder":104,"stem":5181,"tags":5182,"updated":4011,"__hash__":5185},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",{"type":8,"value":4017,"toc":5165},[4018,4022,4025,4028,4032,4035,4045,4048,4055,4120,4127,4129,4136,4405,4415,4419,4422,4425,4450,4464,4467,4490,4496,4499,4502,4505,4510,4517,4742,4747,4791,4795,4798,4805,4808,4858,4864,5093,5103,5105,5156,5159,5162],[4019,4020,4021],"h1",{"id":4021},"背景",[16,4023,4024],{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[16,4026,4027],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[4019,4029,4031],{"id":4030},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[11,4033,4034],{"id":4034},"现象",[16,4036,4037,4038,4041,4042,1178],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[30,4039,4040],{},"post.body.toc"," 上。直接用它渲染目录，发现",[38,4043,4044],{},"只有 h2 标题，h3 全部丢失",[11,4046,4047],{"id":4047},"排查",[16,4049,4050,4051,4054],{},"查了官方文档，发现 ",[30,4052,4053],{},"build.markdown.toc.depth"," 可以控制深度：",[78,4056,4058],{"className":3411,"code":4057,"language":3413,"meta":83,"style":83},"\u002F\u002F nuxt.config.ts\ncontent: {\n  build: {\n    markdown: {\n      toc: { depth: 3 }  \u002F\u002F 包含 h3\n    }\n  }\n}\n",[30,4059,4060,4065,4072,4079,4086,4108,4112,4116],{"__ignoreMap":83},[87,4061,4062],{"class":89,"line":90},[87,4063,4064],{"class":173},"\u002F\u002F nuxt.config.ts\n",[87,4066,4067,4070],{"class":89,"line":104},[87,4068,4069],{"class":93},"content",[87,4071,1985],{"class":148},[87,4073,4074,4077],{"class":89,"line":145},[87,4075,4076],{"class":93},"  build",[87,4078,1985],{"class":148},[87,4080,4081,4084],{"class":89,"line":164},[87,4082,4083],{"class":93},"    markdown",[87,4085,1985],{"class":148},[87,4087,4088,4091,4094,4097,4099,4102,4105],{"class":89,"line":177},[87,4089,4090],{"class":93},"      toc",[87,4092,4093],{"class":148},": { ",[87,4095,4096],{"class":93},"depth",[87,4098,1025],{"class":148},[87,4100,4101],{"class":415},"3",[87,4103,4104],{"class":148}," }  ",[87,4106,4107],{"class":173},"\u002F\u002F 包含 h3\n",[87,4109,4110],{"class":89,"line":189},[87,4111,620],{"class":148},[87,4113,4114],{"class":89,"line":201},[87,4115,2034],{"class":148},[87,4117,4118],{"class":89,"line":211},[87,4119,1062],{"class":148},[16,4121,4122,4123,4126],{},"配置改了，清了缓存重启，",[38,4124,4125],{},"依然只有 h2","。这个配置似乎不起作用。",[11,4128,2446],{"id":2446},[16,4130,4131,4132,4135],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[30,4133,4134],{},"post.body"," 的 AST 里递归提取所有标题：",[78,4137,4139],{"className":3411,"code":4138,"language":3413,"meta":83,"style":83},"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",[30,4140,4141,4183,4202,4215,4220,4278,4307,4337,4362,4366,4390,4394,4401],{"__ignoreMap":83},[87,4142,4143,4145,4148,4150,4153,4155,4158,4160,4163,4165,4168,4171,4173,4176,4178,4180],{"class":89,"line":90},[87,4144,3438],{"class":126},[87,4146,4147],{"class":93}," extractHeadings",[87,4149,219],{"class":148},[87,4151,4152],{"class":341},"node",[87,4154,3086],{"class":126},[87,4156,4157],{"class":415}," any",[87,4159,354],{"class":148},[87,4161,4162],{"class":341},"list",[87,4164,3086],{"class":126},[87,4166,4167],{"class":93}," Heading",[87,4169,4170],{"class":148},"[] ",[87,4172,1671],{"class":126},[87,4174,4175],{"class":148}," [])",[87,4177,3086],{"class":126},[87,4179,4167],{"class":93},[87,4181,4182],{"class":148},"[] {\n",[87,4184,4185,4188,4190,4193,4196,4199],{"class":89,"line":104},[87,4186,4187],{"class":126},"  if",[87,4189,1817],{"class":148},[87,4191,4192],{"class":126},"!",[87,4194,4195],{"class":148},"node) ",[87,4197,4198],{"class":126},"return",[87,4200,4201],{"class":148}," list\n",[87,4203,4204,4206,4209,4212],{"class":89,"line":145},[87,4205,4187],{"class":126},[87,4207,4208],{"class":148}," (Array.",[87,4210,4211],{"class":93},"isArray",[87,4213,4214],{"class":148},"(node)) {\n",[87,4216,4217],{"class":89,"line":164},[87,4218,4219],{"class":173},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[87,4221,4222,4224,4226,4229,4232,4234,4237,4240,4243,4246,4249,4252,4256,4259,4262,4265,4267,4270,4273,4275],{"class":89,"line":177},[87,4223,583],{"class":126},[87,4225,1817],{"class":148},[87,4227,4228],{"class":126},"typeof",[87,4230,4231],{"class":148}," node[",[87,4233,1380],{"class":415},[87,4235,4236],{"class":148},"] ",[87,4238,4239],{"class":126},"===",[87,4241,4242],{"class":97}," 'string'",[87,4244,4245],{"class":126}," &&",[87,4247,4248],{"class":97}," \u002F",[87,4250,4251],{"class":126},"^",[87,4253,4255],{"class":4254},"sPKmS","h",[87,4257,4258],{"class":415},"[1-6]",[87,4260,4261],{"class":126},"$",[87,4263,4264],{"class":97},"\u002F",[87,4266,348],{"class":148},[87,4268,4269],{"class":93},"test",[87,4271,4272],{"class":148},"(node[",[87,4274,1380],{"class":415},[87,4276,4277],{"class":148},"])) {\n",[87,4279,4280,4283,4286,4288,4291,4293,4295,4298,4301,4303,4305],{"class":89,"line":189},[87,4281,4282],{"class":126},"      const",[87,4284,4285],{"class":415}," level",[87,4287,1589],{"class":126},[87,4289,4290],{"class":93}," Number",[87,4292,4272],{"class":148},[87,4294,1380],{"class":415},[87,4296,4297],{"class":148},"].",[87,4299,4300],{"class":93},"charAt",[87,4302,219],{"class":148},[87,4304,1447],{"class":415},[87,4306,1690],{"class":148},[87,4308,4309,4311,4314,4316,4319,4322,4325,4327,4329,4332,4335],{"class":89,"line":201},[87,4310,4282],{"class":126},[87,4312,4313],{"class":415}," text",[87,4315,1589],{"class":126},[87,4317,4318],{"class":93}," extractText",[87,4320,4321],{"class":148},"(node.",[87,4323,4324],{"class":93},"slice",[87,4326,219],{"class":148},[87,4328,1345],{"class":415},[87,4330,4331],{"class":148},")).",[87,4333,4334],{"class":93},"trim",[87,4336,805],{"class":148},[87,4338,4339,4342,4345,4348,4350,4353,4356,4359],{"class":89,"line":211},[87,4340,4341],{"class":148},"      list.",[87,4343,4344],{"class":93},"push",[87,4346,4347],{"class":148},"({ id: node[",[87,4349,1447],{"class":415},[87,4351,4352],{"class":148},"]?.id ",[87,4354,4355],{"class":126},"||",[87,4357,4358],{"class":93}," slugify",[87,4360,4361],{"class":148},"(text), text, level })\n",[87,4363,4364],{"class":89,"line":228},[87,4365,620],{"class":148},[87,4367,4368,4371,4373,4375,4378,4381,4384,4387],{"class":89,"line":390},[87,4369,4370],{"class":126},"    for",[87,4372,1817],{"class":148},[87,4374,1575],{"class":126},[87,4376,4377],{"class":415}," child",[87,4379,4380],{"class":126}," of",[87,4382,4383],{"class":148}," node) ",[87,4385,4386],{"class":93},"extractHeadings",[87,4388,4389],{"class":148},"(child, list)\n",[87,4391,4392],{"class":89,"line":396},[87,4393,2034],{"class":148},[87,4395,4396,4399],{"class":89,"line":419},[87,4397,4398],{"class":126},"  return",[87,4400,4201],{"class":148},[87,4402,4403],{"class":89,"line":617},[87,4404,1062],{"class":148},[16,4406,4407,4408,4410,4411,4414],{},"关键点：Nuxt Content v3 的 body 使用 ",[38,4409,8],{}," 格式，标题节点是数组 ",[30,4412,4413],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[4019,4416,4418],{"id":4417},"问题二图片路径解析错误","问题二：图片路径解析错误",[11,4420,4034],{"id":4421},"现象-1",[16,4423,4424],{},"markdown 里用相对路径引用图片：",[78,4426,4430],{"className":4427,"code":4428,"language":4429,"meta":83,"style":83},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[30,4431,4432],{"__ignoreMap":83},[87,4433,4434,4437,4441,4444,4448],{"class":89,"line":90},[87,4435,4436],{"class":148},"![",[87,4438,4440],{"class":4439},"smiIW","SSHFS挂载效果",[87,4442,4443],{"class":148},"](",[87,4445,4447],{"class":4446},"szbl0","img\u002Fsshfs.avif",[87,4449,231],{"class":148},[16,4451,4452,4453,4456,4457,4460,4461,1178],{},"HTML 输出的 ",[30,4454,4455],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[30,4458,4459],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[30,4462,4463],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[11,4465,4047],{"id":4466},"排查-1",[48,4468,4469,4476,4479,4485],{},[51,4470,4471,4472,4475],{},"检查了 ",[30,4473,4474],{},"\u003Cbase>"," 标签 —— 没有",[51,4477,4478],{},"检查了重定向 —— 没有",[51,4480,4481,4482,4484],{},"直接访问 ",[30,4483,4463],{}," —— 返回 200 OK",[51,4486,4481,4487,4489],{},[30,4488,4459],{}," —— 404",[16,4491,4492,4493,1178],{},"服务器路由没问题，问题是",[38,4494,4495],{},"浏览器解析相对路径时基于了错误的基准 URL",[16,4497,4498],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[11,4500,2446],{"id":4501},"最终方案-1",[16,4503,4504],{},"两步解决：",[16,4506,4507],{},[38,4508,4509],{},"第一步：自定义 ProseImg 组件",[16,4511,4512,4513,4516],{},"创建 ",[30,4514,4515],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[78,4518,4522],{"className":4519,"code":4520,"language":4521,"meta":83,"style":83},"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",[30,4523,4524,4536,4550,4570,4586,4623,4658,4682,4686,4695,4699,4708,4734],{"__ignoreMap":83},[87,4525,4526,4528,4531,4534],{"class":89,"line":90},[87,4527,3089],{"class":148},[87,4529,4530],{"class":3855},"script",[87,4532,4533],{"class":93}," setup",[87,4535,2749],{"class":148},[87,4537,4538,4540,4543,4545,4548],{"class":89,"line":104},[87,4539,1575],{"class":126},[87,4541,4542],{"class":415}," route",[87,4544,1589],{"class":126},[87,4546,4547],{"class":93}," useRoute",[87,4549,805],{"class":148},[87,4551,4552,4554,4557,4559,4562,4565,4568],{"class":89,"line":145},[87,4553,1575],{"class":126},[87,4555,4556],{"class":415}," resolvedSrc",[87,4558,1589],{"class":126},[87,4560,4561],{"class":93}," computed",[87,4563,4564],{"class":148},"(() ",[87,4566,4567],{"class":126},"=>",[87,4569,595],{"class":148},[87,4571,4572,4574,4576,4578,4581,4583],{"class":89,"line":164},[87,4573,4187],{"class":126},[87,4575,1817],{"class":148},[87,4577,4192],{"class":126},[87,4579,4580],{"class":148},"props.src) ",[87,4582,4198],{"class":126},[87,4584,4585],{"class":97}," ''\n",[87,4587,4588,4590,4593,4596,4598,4601,4603,4605,4608,4610,4612,4615,4618,4620],{"class":89,"line":177},[87,4589,4187],{"class":126},[87,4591,4592],{"class":148}," (props.src.",[87,4594,4595],{"class":93},"startsWith",[87,4597,219],{"class":148},[87,4599,4600],{"class":97},"'\u002F'",[87,4602,1208],{"class":148},[87,4604,4355],{"class":126},[87,4606,4607],{"class":148}," props.src.",[87,4609,4595],{"class":93},[87,4611,219],{"class":148},[87,4613,4614],{"class":97},"'http'",[87,4616,4617],{"class":148},")) ",[87,4619,4198],{"class":126},[87,4621,4622],{"class":148}," props.src\n",[87,4624,4625,4628,4631,4633,4636,4639,4641,4643,4647,4649,4651,4653,4656],{"class":89,"line":189},[87,4626,4627],{"class":126},"  const",[87,4629,4630],{"class":415}," base",[87,4632,1589],{"class":126},[87,4634,4635],{"class":148}," route.path.",[87,4637,4638],{"class":93},"replace",[87,4640,219],{"class":148},[87,4642,4264],{"class":97},[87,4644,4646],{"class":4645},"sCnZR","\\\u002F",[87,4648,4261],{"class":126},[87,4650,4264],{"class":97},[87,4652,354],{"class":148},[87,4654,4655],{"class":97},"''",[87,4657,231],{"class":148},[87,4659,4660,4662,4665,4668,4671,4674,4676,4679],{"class":89,"line":201},[87,4661,4398],{"class":126},[87,4663,4664],{"class":97}," `${",[87,4666,4667],{"class":148},"base",[87,4669,4670],{"class":97},"}\u002F${",[87,4672,4673],{"class":148},"props",[87,4675,348],{"class":97},[87,4677,4678],{"class":148},"src",[87,4680,4681],{"class":97},"}`\n",[87,4683,4684],{"class":89,"line":211},[87,4685,422],{"class":148},[87,4687,4688,4691,4693],{"class":89,"line":228},[87,4689,4690],{"class":148},"\u003C\u002F",[87,4692,4530],{"class":3855},[87,4694,2749],{"class":148},[87,4696,4697],{"class":89,"line":390},[87,4698,142],{"emptyLinePlaceholder":141},[87,4700,4701,4703,4706],{"class":89,"line":396},[87,4702,3089],{"class":148},[87,4704,4705],{"class":3855},"template",[87,4707,2749],{"class":148},[87,4709,4710,4713,4715,4718,4720,4723,4726,4728,4731],{"class":89,"line":419},[87,4711,4712],{"class":148},"  \u003C",[87,4714,2426],{"class":3855},[87,4716,4717],{"class":93}," :src",[87,4719,1671],{"class":148},[87,4721,4722],{"class":97},"\"resolvedSrc\"",[87,4724,4725],{"class":93}," :alt",[87,4727,1671],{"class":148},[87,4729,4730],{"class":97},"\"alt\"",[87,4732,4733],{"class":148}," \u002F>\n",[87,4735,4736,4738,4740],{"class":89,"line":617},[87,4737,4690],{"class":148},[87,4739,4705],{"class":3855},[87,4741,2749],{"class":148},[16,4743,4744],{},[38,4745,4746],{},"第二步：启用 MDC prose 组件",[78,4748,4750],{"className":3411,"code":4749,"language":3413,"meta":83,"style":83},"\u002F\u002F nuxt.config.ts\nmdc: {\n  components: {\n    prose: true  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n  }\n}\n",[30,4751,4752,4756,4763,4770,4783,4787],{"__ignoreMap":83},[87,4753,4754],{"class":89,"line":90},[87,4755,4064],{"class":173},[87,4757,4758,4761],{"class":89,"line":104},[87,4759,4760],{"class":93},"mdc",[87,4762,1985],{"class":148},[87,4764,4765,4768],{"class":89,"line":145},[87,4766,4767],{"class":93},"  components",[87,4769,1985],{"class":148},[87,4771,4772,4775,4777,4780],{"class":89,"line":164},[87,4773,4774],{"class":93},"    prose",[87,4776,1025],{"class":148},[87,4778,4779],{"class":415},"true",[87,4781,4782],{"class":173},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[87,4784,4785],{"class":89,"line":177},[87,4786,2034],{"class":148},[87,4788,4789],{"class":89,"line":189},[87,4790,1062],{"class":148},[4019,4792,4794],{"id":4793},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[11,4796,4034],{"id":4797},"现象-2",[16,4799,4800,4801,4804],{},"原站（SvelteKit）的图片放在 ",[30,4802,4803],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[11,4806,4807],{"id":4807},"方案对比",[2043,4809,4810,4823],{},[2046,4811,4812],{},[2049,4813,4814,4817,4820],{},[2052,4815,4816],{},"方案",[2052,4818,4819],{},"优点",[2052,4821,4822],{},"缺点",[2059,4824,4825,4836,4847],{},[2049,4826,4827,4830,4833],{},[2064,4828,4829],{},"复制到 public\u002F",[2064,4831,4832],{},"简单",[2064,4834,4835],{},"文件重复，需同步",[2049,4837,4838,4841,4844],{},[2064,4839,4840],{},"Vite 插件拦截",[2064,4842,4843],{},"原站方案",[2064,4845,4846],{},"Nuxt 不直接支持",[2049,4848,4849,4852,4855],{},[2064,4850,4851],{},"Server Route",[2064,4853,4854],{},"不复制文件",[2064,4856,4857],{},"需要手动写路由",[16,4859,4860,4861,4863],{},"最终选择 ",[38,4862,4851],{},"，和原站的 Vite 插件思路一致：",[78,4865,4867],{"className":3411,"code":4866,"language":3413,"meta":83,"style":83},"\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",[30,4868,4869,4874,4900,4915,4979,4993,4997,5019,5045,5063,5082,5089],{"__ignoreMap":83},[87,4870,4871],{"class":89,"line":90},[87,4872,4873],{"class":173},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[87,4875,4876,4878,4881,4884,4886,4889,4891,4894,4896,4898],{"class":89,"line":104},[87,4877,2268],{"class":126},[87,4879,4880],{"class":126}," default",[87,4882,4883],{"class":93}," defineEventHandler",[87,4885,219],{"class":148},[87,4887,4888],{"class":126},"async",[87,4890,1817],{"class":148},[87,4892,4893],{"class":341},"event",[87,4895,1208],{"class":148},[87,4897,4567],{"class":126},[87,4899,595],{"class":148},[87,4901,4902,4904,4907,4909,4912],{"class":89,"line":145},[87,4903,4627],{"class":126},[87,4905,4906],{"class":415}," url",[87,4908,1589],{"class":126},[87,4910,4911],{"class":93}," getRequestURL",[87,4913,4914],{"class":148},"(event)\n",[87,4916,4917,4919,4922,4924,4927,4930,4932,4934,4936,4938,4941,4943,4945,4948,4950,4953,4956,4959,4961,4963,4965,4967,4969,4971,4973,4975,4977],{"class":89,"line":164},[87,4918,4627],{"class":126},[87,4920,4921],{"class":415}," match",[87,4923,1589],{"class":126},[87,4925,4926],{"class":148}," url.pathname.",[87,4928,4929],{"class":93},"match",[87,4931,219],{"class":148},[87,4933,4264],{"class":97},[87,4935,4251],{"class":126},[87,4937,4646],{"class":4645},[87,4939,4940],{"class":4254},"posts",[87,4942,4646],{"class":4645},[87,4944,219],{"class":4254},[87,4946,4947],{"class":415},"[",[87,4949,4251],{"class":126},[87,4951,4952],{"class":415},"\u002F]",[87,4954,4955],{"class":126},"+",[87,4957,4958],{"class":4254},")",[87,4960,4646],{"class":4645},[87,4962,2426],{"class":4254},[87,4964,4646],{"class":4645},[87,4966,219],{"class":4254},[87,4968,348],{"class":415},[87,4970,4955],{"class":126},[87,4972,4958],{"class":4254},[87,4974,4261],{"class":126},[87,4976,4264],{"class":97},[87,4978,231],{"class":148},[87,4980,4981,4983,4985,4987,4990],{"class":89,"line":177},[87,4982,4187],{"class":126},[87,4984,1817],{"class":148},[87,4986,4192],{"class":126},[87,4988,4989],{"class":148},"match) ",[87,4991,4992],{"class":126},"return\n",[87,4994,4995],{"class":89,"line":189},[87,4996,142],{"emptyLinePlaceholder":141},[87,4998,4999,5001,5004,5007,5009,5012,5014,5016],{"class":89,"line":201},[87,5000,4627],{"class":126},[87,5002,5003],{"class":148}," [, ",[87,5005,5006],{"class":415},"slug",[87,5008,354],{"class":148},[87,5010,5011],{"class":415},"filename",[87,5013,4236],{"class":148},[87,5015,1671],{"class":126},[87,5017,5018],{"class":148}," match\n",[87,5020,5021,5023,5026,5028,5031,5033,5036,5039,5042],{"class":89,"line":211},[87,5022,4627],{"class":126},[87,5024,5025],{"class":415}," filePath",[87,5027,1589],{"class":126},[87,5029,5030],{"class":93}," resolve",[87,5032,219],{"class":148},[87,5034,5035],{"class":97},"'content\u002Fposts'",[87,5037,5038],{"class":148},", slug, ",[87,5040,5041],{"class":97},"'img'",[87,5043,5044],{"class":148},", filename)\n",[87,5046,5047,5049,5052,5054,5057,5060],{"class":89,"line":228},[87,5048,4627],{"class":126},[87,5050,5051],{"class":415}," data",[87,5053,1589],{"class":126},[87,5055,5056],{"class":126}," await",[87,5058,5059],{"class":93}," readFile",[87,5061,5062],{"class":148},"(filePath)\n",[87,5064,5065,5068,5071,5074,5076,5079],{"class":89,"line":390},[87,5066,5067],{"class":93},"  setResponseHeader",[87,5069,5070],{"class":148},"(event, ",[87,5072,5073],{"class":97},"'content-type'",[87,5075,354],{"class":148},[87,5077,5078],{"class":415},"MIME",[87,5080,5081],{"class":148},"[ext])\n",[87,5083,5084,5086],{"class":89,"line":396},[87,5085,4398],{"class":126},[87,5087,5088],{"class":148}," data\n",[87,5090,5091],{"class":89,"line":419},[87,5092,422],{"class":148},[16,5094,5095,5096,5098,5099,5102],{},"请求 ",[30,5097,4463],{}," → 读取 ",[30,5100,5101],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[4019,5104,2041],{"id":2041},[2043,5106,5107,5118],{},[2046,5108,5109],{},[2049,5110,5111,5113,5116],{},[2052,5112,1156],{},[2052,5114,5115],{},"根因",[2052,5117,4816],{},[2059,5119,5120,5134,5145],{},[2049,5121,5122,5125,5131],{},[2064,5123,5124],{},"TOC 只有 h2",[2064,5126,5127,5130],{},[30,5128,5129],{},"toc.depth"," 配置不生效",[2064,5132,5133],{},"从 body AST 手动提取",[2049,5135,5136,5139,5142],{},[2064,5137,5138],{},"图片路径错误",[2064,5140,5141],{},"SPA 路由导致相对路径解析基准错误",[2064,5143,5144],{},"ProseImg 组件 + mdc.prose 配置",[2049,5146,5147,5150,5153],{},[2064,5148,5149],{},"content 图片 404",[2064,5151,5152],{},"content 目录不暴露给浏览器",[2064,5154,5155],{},"Server Route 直接读取返回",[16,5157,5158],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[16,5160,5161],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[2124,5163,5164],{},"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":83,"searchDepth":104,"depth":145,"links":5166},[5167,5168,5169,5170,5171,5172,5173,5174],{"id":4034,"depth":104,"text":4034},{"id":4047,"depth":104,"text":4047},{"id":2446,"depth":104,"text":2446},{"id":4421,"depth":104,"text":4034},{"id":4466,"depth":104,"text":4047},{"id":4501,"depth":104,"text":2446},{"id":4797,"depth":104,"text":4034},{"id":4807,"depth":104,"text":4807},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":4015,"description":5175},"posts\u002Fnuxt-content-lessons\u002Findex",[3504,5183,5184],"踩坑","Nuxt Content","vbSt6ND1z7xt7L8FxgZ8Qe3EdxV9InsRerN3E23GC7s",{"id":5187,"title":5188,"author":3613,"body":5189,"description":5453,"difficulty":2147,"draft":2148,"extension":2149,"image":5454,"meta":5455,"navigation":141,"path":5456,"pinned":2148,"published":5457,"seo":5458,"series":4007,"seriesOrder":145,"stem":5459,"tags":5460,"updated":4011,"__hash__":5463},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题",{"type":8,"value":5190,"toc":5446},[5191,5194,5197,5212,5215,5218,5221,5262,5273,5280,5302,5309,5313,5316,5319,5321,5324,5347,5358,5414,5419,5432,5434,5440,5443],[11,5192,5193],{"id":5193},"问题描述",[16,5195,5196],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[3705,5198,5199,5209],{},[51,5200,5201,5202,5205,5206],{},"首次通过客户端导航（点击 ",[30,5203,5204],{},"\u003CNuxtLink>","）跳转到某个页面时，",[38,5207,5208],{},"所有链接文字都会出现下划线",[51,5210,5211],{},"手动刷新页面后，下划线消失，样式恢复正常",[16,5213,5214],{},"这个问题在开发模式和生产构建后都会出现。",[11,5216,5217],{"id":5217},"原因分析",[16,5219,5220],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[78,5222,5226],{"className":5223,"code":5224,"language":5225,"meta":83,"style":83},"language-css shiki shiki-themes github-light github-dark","a {\n  color: inherit;\n  text-decoration: inherit;\n}\n","css",[30,5227,5228,5234,5247,5258],{"__ignoreMap":83},[87,5229,5230,5232],{"class":89,"line":90},[87,5231,3974],{"class":3855},[87,5233,595],{"class":148},[87,5235,5236,5239,5241,5244],{"class":89,"line":104},[87,5237,5238],{"class":415},"  color",[87,5240,1025],{"class":148},[87,5242,5243],{"class":415},"inherit",[87,5245,5246],{"class":148},";\n",[87,5248,5249,5252,5254,5256],{"class":89,"line":145},[87,5250,5251],{"class":415},"  text-decoration",[87,5253,1025],{"class":148},[87,5255,5243],{"class":415},[87,5257,5246],{"class":148},[87,5259,5260],{"class":89,"line":164},[87,5261,1062],{"class":148},[16,5263,5264,5265,5268,5269,5272],{},"正常情况下，这会让链接继承父元素的 ",[30,5266,5267],{},"text-decoration","（通常是 ",[30,5270,5271],{},"none","），从而去掉浏览器默认的下划线。",[16,5274,5275,5276,5279],{},"问题出在 ",[38,5277,5278],{},"Nuxt 的客户端导航机制","：",[48,5281,5282,5288,5291,5296,5299],{},[51,5283,5284,5285,5287],{},"用户点击 ",[30,5286,5204],{},"，Vue Router 拦截导航",[51,5289,5290],{},"新页面组件异步加载并渲染",[51,5292,5293],{},[38,5294,5295],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[51,5297,5298],{},"此时浏览器使用默认样式（链接带下划线）",[51,5300,5301],{},"CSS 加载完成后，下划线消失",[16,5303,5304,5305,5308],{},"这就是典型的 ",[38,5306,5307],{},"FOUC（Flash of Unstyled Content）"," 问题。",[11,5310,5312],{"id":5311},"为什么刷新就正常了","为什么刷新就正常了？",[16,5314,5315],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[16,5317,5318],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[11,5320,2975],{"id":2975},[16,5322,5323],{},"尝试了多种方案：",[3705,5325,5326,5332,5338,5344],{},[51,5327,5328,5331],{},[30,5329,5330],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[51,5333,5334,5337],{},[30,5335,5336],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[51,5339,5340,5343],{},[30,5341,5342],{},"features.inlineStyles: true"," — 无效",[51,5345,5346],{},"页面过渡动画 — 只是掩盖，不能根治",[16,5348,5349,5350,5353,5354,5357],{},"最终有效的方案是在 ",[30,5351,5352],{},"app.vue"," 中用 ",[30,5355,5356],{},"!important"," 强制覆盖：",[78,5359,5361],{"className":4519,"code":5360,"language":4521,"meta":83,"style":83},"\u003Cstyle>\na:not(.prose a) {\n  text-decoration-line: none !important;\n}\n\u003C\u002Fstyle>\n",[30,5362,5363,5371,5388,5402,5406],{"__ignoreMap":83},[87,5364,5365,5367,5369],{"class":89,"line":90},[87,5366,3089],{"class":148},[87,5368,2124],{"class":3855},[87,5370,2749],{"class":148},[87,5372,5373,5375,5378,5380,5383,5386],{"class":89,"line":104},[87,5374,3974],{"class":3855},[87,5376,5377],{"class":93},":not",[87,5379,219],{"class":148},[87,5381,5382],{"class":93},".prose",[87,5384,5385],{"class":3855}," a",[87,5387,387],{"class":148},[87,5389,5390,5393,5395,5397,5400],{"class":89,"line":145},[87,5391,5392],{"class":415},"  text-decoration-line",[87,5394,1025],{"class":148},[87,5396,5271],{"class":415},[87,5398,5399],{"class":126}," !important",[87,5401,5246],{"class":148},[87,5403,5404],{"class":89,"line":164},[87,5405,1062],{"class":148},[87,5407,5408,5410,5412],{"class":89,"line":177},[87,5409,4690],{"class":148},[87,5411,2124],{"class":3855},[87,5413,2749],{"class":148},[16,5415,5416],{},[38,5417,5418],{},"关键点：",[3705,5420,5421,5426],{},[51,5422,5423,5425],{},[30,5424,5356],{}," 是为了对抗 Tailwind CSS layers 的优先级",[51,5427,5428,5431],{},[30,5429,5430],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[11,5433,2041],{"id":2041},[16,5435,5436,5437,5439],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[30,5438,5356],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[16,5441,5442],{},"如果你有更好的方案，欢迎在项目仓库的 Issues 中提出！",[2124,5444,5445],{},"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":83,"searchDepth":104,"depth":145,"links":5447},[5448,5449,5450,5451,5452],{"id":5193,"depth":104,"text":5193},{"id":5217,"depth":104,"text":5217},{"id":5311,"depth":104,"text":5312},{"id":2975,"depth":104,"text":2975},{"id":2041,"depth":104,"text":2041},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":5188,"description":5453},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[3504,5461,5462,5183],"Tailwind CSS","CSS","WMLpTEsIoJqufwEfXyIiBgUYxcPY2XkvOwqAD4hbe6g",{"id":4,"title":5,"author":6,"body":5465,"description":2146,"difficulty":2147,"draft":2148,"extension":2149,"image":2150,"meta":7188,"navigation":141,"path":2152,"pinned":2148,"published":2153,"seo":7189,"series":2155,"seriesOrder":90,"stem":2156,"tags":7190,"updated":2163,"__hash__":2164},{"type":8,"value":5466,"toc":7169},[5467,5469,5471,5473,5475,5479,5483,5485,5487,5501,5503,5505,5525,5527,5605,5613,5615,5747,5749,5943,5945,5951,6035,6039,6041,6281,6283,6351,6353,6355,6357,6363,6365,6369,6687,6853,6855,6901,6903,7039,7041,7043,7103,7105,7161,7167],[11,5468,14],{"id":13},[16,5470,18],{},[16,5472,21],{},[11,5474,25],{"id":24},[16,5476,28,5477,33],{},[30,5478,32],{},[16,5480,36,5481],{},[38,5482,40],{},[11,5484,43],{"id":43},[16,5486,46],{},[48,5488,5489,5493,5497],{},[51,5490,5491,56],{},[38,5492,55],{},[51,5494,5495,62],{},[38,5496,61],{},[51,5498,5499,68],{},[38,5500,67],{},[11,5502,71],{"id":71},[73,5504,76],{"id":75},[78,5506,5507],{"className":80,"code":81,"language":82,"meta":83,"style":83},[30,5508,5509,5517],{"__ignoreMap":83},[87,5510,5511,5513,5515],{"class":89,"line":90},[87,5512,94],{"class":93},[87,5514,98],{"class":97},[87,5516,101],{"class":97},[87,5518,5519,5521,5523],{"class":89,"line":104},[87,5520,94],{"class":93},[87,5522,98],{"class":97},[87,5524,111],{"class":97},[73,5526,115],{"id":114},[78,5528,5529],{"className":118,"code":119,"language":94,"meta":83,"style":83},[30,5530,5531,5541,5545,5557,5565,5573,5581,5589,5601],{"__ignoreMap":83},[87,5532,5533,5535,5537,5539],{"class":89,"line":90},[87,5534,127],{"class":126},[87,5536,130],{"class":97},[87,5538,133],{"class":93},[87,5540,136],{"class":97},[87,5542,5543],{"class":89,"line":104},[87,5544,142],{"emptyLinePlaceholder":141},[87,5546,5547,5549,5551,5553,5555],{"class":89,"line":145},[87,5548,149],{"class":148},[87,5550,152],{"class":126},[87,5552,155],{"class":148},[87,5554,158],{"class":93},[87,5556,161],{"class":148},[87,5558,5559,5561,5563],{"class":89,"line":164},[87,5560,167],{"class":97},[87,5562,170],{"class":148},[87,5564,174],{"class":173},[87,5566,5567,5569,5571],{"class":89,"line":177},[87,5568,180],{"class":97},[87,5570,183],{"class":148},[87,5572,186],{"class":173},[87,5574,5575,5577,5579],{"class":89,"line":189},[87,5576,192],{"class":148},[87,5578,195],{"class":93},[87,5580,198],{"class":148},[87,5582,5583,5585,5587],{"class":89,"line":201},[87,5584,192],{"class":148},[87,5586,206],{"class":93},[87,5588,198],{"class":148},[87,5590,5591,5593,5595,5597,5599],{"class":89,"line":211},[87,5592,192],{"class":148},[87,5594,216],{"class":93},[87,5596,219],{"class":148},[87,5598,222],{"class":97},[87,5600,225],{"class":148},[87,5602,5603],{"class":89,"line":228},[87,5604,231],{"class":148},[16,5606,5607,236,5609,240,5611,244],{},[30,5608,158],{},[30,5610,239],{},[30,5612,243],{},[73,5614,248],{"id":247},[78,5616,5617],{"className":118,"code":251,"language":94,"meta":83,"style":83},[30,5618,5619,5629,5633,5637,5653,5665,5669,5673,5677,5723,5727,5743],{"__ignoreMap":83},[87,5620,5621,5623,5625,5627],{"class":89,"line":90},[87,5622,127],{"class":126},[87,5624,130],{"class":97},[87,5626,262],{"class":93},[87,5628,136],{"class":97},[87,5630,5631],{"class":89,"line":104},[87,5632,142],{"emptyLinePlaceholder":141},[87,5634,5635],{"class":89,"line":145},[87,5636,273],{"class":173},[87,5638,5639,5641,5643,5645,5647,5649,5651],{"class":89,"line":164},[87,5640,278],{"class":148},[87,5642,152],{"class":126},[87,5644,283],{"class":148},[87,5646,286],{"class":93},[87,5648,219],{"class":148},[87,5650,291],{"class":97},[87,5652,294],{"class":148},[87,5654,5655,5657,5659,5661,5663],{"class":89,"line":177},[87,5656,299],{"class":148},[87,5658,302],{"class":93},[87,5660,219],{"class":148},[87,5662,307],{"class":97},[87,5664,225],{"class":148},[87,5666,5667],{"class":89,"line":189},[87,5668,231],{"class":148},[87,5670,5671],{"class":89,"line":201},[87,5672,142],{"emptyLinePlaceholder":141},[87,5674,5675],{"class":89,"line":211},[87,5676,322],{"class":173},[87,5678,5679,5681,5683,5685,5687,5689,5691,5693,5695,5697,5699,5701,5703,5705,5707,5709,5711,5713,5715,5717,5719,5721],{"class":89,"line":228},[87,5680,327],{"class":148},[87,5682,330],{"class":93},[87,5684,333],{"class":148},[87,5686,336],{"class":126},[87,5688,219],{"class":148},[87,5690,342],{"class":341},[87,5692,345],{"class":93},[87,5694,348],{"class":148},[87,5696,351],{"class":93},[87,5698,354],{"class":148},[87,5700,357],{"class":341},[87,5702,360],{"class":93},[87,5704,348],{"class":148},[87,5706,365],{"class":93},[87,5708,368],{"class":148},[87,5710,371],{"class":126},[87,5712,374],{"class":93},[87,5714,348],{"class":148},[87,5716,379],{"class":93},[87,5718,354],{"class":148},[87,5720,384],{"class":126},[87,5722,387],{"class":148},[87,5724,5725],{"class":89,"line":390},[87,5726,393],{"class":173},[87,5728,5729,5731,5733,5735,5737,5739,5741],{"class":89,"line":396},[87,5730,399],{"class":126},[87,5732,283],{"class":148},[87,5734,404],{"class":93},[87,5736,219],{"class":148},[87,5738,409],{"class":97},[87,5740,412],{"class":148},[87,5742,416],{"class":415},[87,5744,5745],{"class":89,"line":419},[87,5746,422],{"class":148},[16,5748,425],{},[78,5750,5751],{"className":118,"code":428,"language":94,"meta":83,"style":83},[30,5752,5753,5769,5781,5793,5801,5813,5817,5821,5825,5871,5887,5899,5915,5919,5923,5939],{"__ignoreMap":83},[87,5754,5755,5757,5759,5761,5763,5765,5767],{"class":89,"line":90},[87,5756,435],{"class":148},[87,5758,152],{"class":126},[87,5760,283],{"class":148},[87,5762,286],{"class":93},[87,5764,219],{"class":148},[87,5766,446],{"class":97},[87,5768,294],{"class":148},[87,5770,5771,5773,5775,5777,5779],{"class":89,"line":104},[87,5772,299],{"class":148},[87,5774,302],{"class":93},[87,5776,219],{"class":148},[87,5778,459],{"class":97},[87,5780,225],{"class":148},[87,5782,5783,5785,5787,5789,5791],{"class":89,"line":145},[87,5784,299],{"class":148},[87,5786,468],{"class":93},[87,5788,219],{"class":148},[87,5790,473],{"class":97},[87,5792,294],{"class":148},[87,5794,5795,5797,5799],{"class":89,"line":164},[87,5796,480],{"class":148},[87,5798,483],{"class":93},[87,5800,198],{"class":148},[87,5802,5803,5805,5807,5809,5811],{"class":89,"line":177},[87,5804,480],{"class":148},[87,5806,492],{"class":93},[87,5808,219],{"class":148},[87,5810,497],{"class":97},[87,5812,225],{"class":148},[87,5814,5815],{"class":89,"line":189},[87,5816,504],{"class":148},[87,5818,5819],{"class":89,"line":201},[87,5820,231],{"class":148},[87,5822,5823],{"class":89,"line":211},[87,5824,142],{"emptyLinePlaceholder":141},[87,5826,5827,5829,5831,5833,5835,5837,5839,5841,5843,5845,5847,5849,5851,5853,5855,5857,5859,5861,5863,5865,5867,5869],{"class":89,"line":228},[87,5828,327],{"class":148},[87,5830,330],{"class":93},[87,5832,521],{"class":148},[87,5834,336],{"class":126},[87,5836,219],{"class":148},[87,5838,342],{"class":341},[87,5840,345],{"class":93},[87,5842,348],{"class":148},[87,5844,351],{"class":93},[87,5846,354],{"class":148},[87,5848,357],{"class":341},[87,5850,360],{"class":93},[87,5852,348],{"class":148},[87,5854,365],{"class":93},[87,5856,368],{"class":148},[87,5858,371],{"class":126},[87,5860,374],{"class":93},[87,5862,348],{"class":148},[87,5864,379],{"class":93},[87,5866,354],{"class":148},[87,5868,384],{"class":126},[87,5870,387],{"class":148},[87,5872,5873,5875,5877,5879,5881,5883,5885],{"class":89,"line":390},[87,5874,564],{"class":148},[87,5876,152],{"class":126},[87,5878,569],{"class":148},[87,5880,572],{"class":93},[87,5882,219],{"class":148},[87,5884,473],{"class":97},[87,5886,231],{"class":148},[87,5888,5889,5891,5893,5895,5897],{"class":89,"line":396},[87,5890,583],{"class":126},[87,5892,586],{"class":148},[87,5894,589],{"class":126},[87,5896,592],{"class":415},[87,5898,595],{"class":148},[87,5900,5901,5903,5905,5907,5909,5911,5913],{"class":89,"line":419},[87,5902,600],{"class":126},[87,5904,283],{"class":148},[87,5906,605],{"class":93},[87,5908,219],{"class":148},[87,5910,610],{"class":97},[87,5912,412],{"class":148},[87,5914,416],{"class":415},[87,5916,5917],{"class":89,"line":617},[87,5918,620],{"class":148},[87,5920,5921],{"class":89,"line":623},[87,5922,626],{"class":173},[87,5924,5925,5927,5929,5931,5933,5935,5937],{"class":89,"line":629},[87,5926,399],{"class":126},[87,5928,283],{"class":148},[87,5930,404],{"class":93},[87,5932,219],{"class":148},[87,5934,640],{"class":97},[87,5936,412],{"class":148},[87,5938,416],{"class":415},[87,5940,5941],{"class":89,"line":647},[87,5942,422],{"class":148},[73,5944,653],{"id":652},[16,5946,656,5947,660,5949,664],{},[30,5948,659],{},[30,5950,663],{},[78,5952,5953],{"className":118,"code":667,"language":94,"meta":83,"style":83},[30,5954,5955,5965,5969,5981,5993,6007,6011,6015,6019],{"__ignoreMap":83},[87,5956,5957,5959,5961,5963],{"class":89,"line":90},[87,5958,127],{"class":126},[87,5960,130],{"class":97},[87,5962,133],{"class":93},[87,5964,136],{"class":97},[87,5966,5967],{"class":89,"line":104},[87,5968,142],{"emptyLinePlaceholder":141},[87,5970,5971,5973,5975,5977,5979],{"class":89,"line":145},[87,5972,688],{"class":148},[87,5974,152],{"class":126},[87,5976,155],{"class":148},[87,5978,695],{"class":93},[87,5980,698],{"class":148},[87,5982,5983,5985,5987,5989,5991],{"class":89,"line":164},[87,5984,192],{"class":148},[87,5986,705],{"class":93},[87,5988,219],{"class":148},[87,5990,710],{"class":97},[87,5992,225],{"class":148},[87,5994,5995,5997,5999,6001,6003,6005],{"class":89,"line":177},[87,5996,192],{"class":148},[87,5998,719],{"class":93},[87,6000,219],{"class":148},[87,6002,724],{"class":415},[87,6004,371],{"class":126},[87,6006,729],{"class":148},[87,6008,6009],{"class":89,"line":189},[87,6010,231],{"class":148},[87,6012,6013],{"class":89,"line":201},[87,6014,142],{"emptyLinePlaceholder":141},[87,6016,6017],{"class":89,"line":211},[87,6018,742],{"class":173},[87,6020,6021,6023,6025,6027,6029,6031,6033],{"class":89,"line":228},[87,6022,747],{"class":148},[87,6024,750],{"class":93},[87,6026,219],{"class":148},[87,6028,710],{"class":97},[87,6030,757],{"class":148},[87,6032,760],{"class":93},[87,6034,763],{"class":148},[16,6036,766,6037,770],{},[30,6038,769],{},[73,6040,774],{"id":773},[78,6042,6043],{"className":118,"code":777,"language":94,"meta":83,"style":83},[30,6044,6045,6053,6065,6069,6073,6093,6101,6109,6113,6117,6121,6127,6131,6135,6147,6159,6173,6177,6193,6197,6201,6229,6257,6261,6265,6277],{"__ignoreMap":83},[87,6046,6047,6049,6051],{"class":89,"line":90},[87,6048,336],{"class":126},[87,6050,786],{"class":93},[87,6052,789],{"class":148},[87,6054,6055,6057,6059,6061,6063],{"class":89,"line":104},[87,6056,794],{"class":148},[87,6058,152],{"class":126},[87,6060,799],{"class":148},[87,6062,802],{"class":93},[87,6064,805],{"class":148},[87,6066,6067],{"class":89,"line":145},[87,6068,142],{"emptyLinePlaceholder":141},[87,6070,6071],{"class":89,"line":164},[87,6072,814],{"class":173},[87,6074,6075,6077,6079,6081,6083,6085,6087,6089,6091],{"class":89,"line":177},[87,6076,819],{"class":148},[87,6078,152],{"class":126},[87,6080,155],{"class":148},[87,6082,158],{"class":93},[87,6084,219],{"class":148},[87,6086,830],{"class":97},[87,6088,354],{"class":148},[87,6090,835],{"class":97},[87,6092,294],{"class":148},[87,6094,6095,6097,6099],{"class":89,"line":189},[87,6096,842],{"class":148},[87,6098,195],{"class":93},[87,6100,198],{"class":148},[87,6102,6103,6105,6107],{"class":89,"line":201},[87,6104,842],{"class":148},[87,6106,206],{"class":93},[87,6108,198],{"class":148},[87,6110,6111],{"class":89,"line":211},[87,6112,859],{"class":148},[87,6114,6115],{"class":89,"line":228},[87,6116,142],{"emptyLinePlaceholder":141},[87,6118,6119],{"class":89,"line":390},[87,6120,868],{"class":173},[87,6122,6123,6125],{"class":89,"line":396},[87,6124,873],{"class":93},[87,6126,876],{"class":148},[87,6128,6129],{"class":89,"line":419},[87,6130,142],{"emptyLinePlaceholder":141},[87,6132,6133],{"class":89,"line":617},[87,6134,885],{"class":173},[87,6136,6137,6139,6141,6143,6145],{"class":89,"line":623},[87,6138,890],{"class":148},[87,6140,152],{"class":126},[87,6142,155],{"class":148},[87,6144,695],{"class":93},[87,6146,698],{"class":148},[87,6148,6149,6151,6153,6155,6157],{"class":89,"line":629},[87,6150,842],{"class":148},[87,6152,705],{"class":93},[87,6154,219],{"class":148},[87,6156,710],{"class":97},[87,6158,225],{"class":148},[87,6160,6161,6163,6165,6167,6169,6171],{"class":89,"line":647},[87,6162,842],{"class":148},[87,6164,719],{"class":93},[87,6166,219],{"class":148},[87,6168,724],{"class":415},[87,6170,371],{"class":126},[87,6172,729],{"class":148},[87,6174,6175],{"class":89,"line":927},[87,6176,859],{"class":148},[87,6178,6179,6181,6183,6185,6187,6189,6191],{"class":89,"line":932},[87,6180,935],{"class":148},[87,6182,750],{"class":93},[87,6184,219],{"class":148},[87,6186,710],{"class":97},[87,6188,757],{"class":148},[87,6190,760],{"class":93},[87,6192,948],{"class":148},[87,6194,6195],{"class":89,"line":951},[87,6196,142],{"emptyLinePlaceholder":141},[87,6198,6199],{"class":89,"line":956},[87,6200,959],{"class":173},[87,6202,6203,6205,6207,6209,6211,6213,6215,6217,6219,6221,6223,6225,6227],{"class":89,"line":962},[87,6204,935],{"class":148},[87,6206,967],{"class":93},[87,6208,219],{"class":148},[87,6210,972],{"class":97},[87,6212,354],{"class":148},[87,6214,336],{"class":126},[87,6216,219],{"class":148},[87,6218,981],{"class":341},[87,6220,984],{"class":126},[87,6222,987],{"class":93},[87,6224,348],{"class":148},[87,6226,351],{"class":93},[87,6228,387],{"class":148},[87,6230,6231,6233,6235,6237,6239,6241,6243,6245,6247,6249,6251,6253,6255],{"class":89,"line":996},[87,6232,999],{"class":148},[87,6234,1002],{"class":93},[87,6236,219],{"class":148},[87,6238,1007],{"class":415},[87,6240,354],{"class":148},[87,6242,987],{"class":93},[87,6244,348],{"class":148},[87,6246,1016],{"class":93},[87,6248,1019],{"class":148},[87,6250,1022],{"class":97},[87,6252,1025],{"class":148},[87,6254,1028],{"class":97},[87,6256,422],{"class":148},[87,6258,6259],{"class":89,"line":1033},[87,6260,1036],{"class":148},[87,6262,6263],{"class":89,"line":1039},[87,6264,142],{"emptyLinePlaceholder":141},[87,6266,6267,6269,6271,6273,6275],{"class":89,"line":1044},[87,6268,935],{"class":148},[87,6270,1049],{"class":93},[87,6272,219],{"class":148},[87,6274,1054],{"class":97},[87,6276,231],{"class":148},[87,6278,6279],{"class":89,"line":1059},[87,6280,1062],{"class":148},[16,6282,1065],{},[78,6284,6285],{"className":80,"code":1068,"language":82,"meta":83,"style":83},[30,6286,6287,6291,6303,6311,6317,6321,6325,6337,6345],{"__ignoreMap":83},[87,6288,6289],{"class":89,"line":90},[87,6290,1075],{"class":173},[87,6292,6293,6295,6297,6299,6301],{"class":89,"line":104},[87,6294,1080],{"class":93},[87,6296,1083],{"class":415},[87,6298,1086],{"class":97},[87,6300,1089],{"class":97},[87,6302,1092],{"class":415},[87,6304,6305,6307,6309],{"class":89,"line":145},[87,6306,1097],{"class":415},[87,6308,1100],{"class":97},[87,6310,1092],{"class":415},[87,6312,6313,6315],{"class":89,"line":164},[87,6314,1107],{"class":415},[87,6316,1110],{"class":97},[87,6318,6319],{"class":89,"line":177},[87,6320,142],{"emptyLinePlaceholder":141},[87,6322,6323],{"class":89,"line":189},[87,6324,1119],{"class":173},[87,6326,6327,6329,6331,6333,6335],{"class":89,"line":201},[87,6328,1080],{"class":93},[87,6330,1083],{"class":415},[87,6332,1086],{"class":97},[87,6334,1089],{"class":97},[87,6336,1092],{"class":415},[87,6338,6339,6341,6343],{"class":89,"line":211},[87,6340,1097],{"class":415},[87,6342,1100],{"class":97},[87,6344,1092],{"class":415},[87,6346,6347,6349],{"class":89,"line":228},[87,6348,1107],{"class":415},[87,6350,1146],{"class":97},[11,6352,1150],{"id":1149},[16,6354,1153],{},[73,6356,1156],{"id":1156},[16,6358,1159,6359,1163,6361,1167],{},[30,6360,1162],{},[30,6362,1166],{},[73,6364,1171],{"id":1170},[16,6366,1174,6367,1178],{},[30,6368,1177],{},[78,6370,6371],{"className":118,"code":1181,"language":94,"meta":83,"style":83},[30,6372,6373,6377,6401,6421,6437,6449,6477,6481,6485,6489,6509,6539,6567,6571,6575,6579,6595,6607,6635,6639,6643,6647,6659,6671,6679,6683],{"__ignoreMap":83},[87,6374,6375],{"class":89,"line":90},[87,6376,1188],{"class":173},[87,6378,6379,6381,6383,6385,6387,6389,6391,6393,6395,6397,6399],{"class":89,"line":104},[87,6380,336],{"class":126},[87,6382,1195],{"class":93},[87,6384,219],{"class":148},[87,6386,1200],{"class":341},[87,6388,984],{"class":126},[87,6390,1205],{"class":93},[87,6392,1208],{"class":148},[87,6394,987],{"class":93},[87,6396,348],{"class":148},[87,6398,1215],{"class":93},[87,6400,595],{"class":148},[87,6402,6403,6405,6407,6409,6411,6413,6415,6417,6419],{"class":89,"line":145},[87,6404,399],{"class":126},[87,6406,1224],{"class":126},[87,6408,219],{"class":148},[87,6410,981],{"class":341},[87,6412,984],{"class":126},[87,6414,987],{"class":93},[87,6416,348],{"class":148},[87,6418,351],{"class":93},[87,6420,387],{"class":148},[87,6422,6423,6425,6427,6429,6431,6433,6435],{"class":89,"line":164},[87,6424,1243],{"class":148},[87,6426,152],{"class":126},[87,6428,1248],{"class":148},[87,6430,1251],{"class":93},[87,6432,219],{"class":148},[87,6434,1256],{"class":97},[87,6436,231],{"class":148},[87,6438,6439,6441,6443,6445,6447],{"class":89,"line":177},[87,6440,1263],{"class":126},[87,6442,1266],{"class":148},[87,6444,1269],{"class":126},[87,6446,1272],{"class":97},[87,6448,595],{"class":148},[87,6450,6451,6453,6455,6457,6459,6461,6463,6465,6467,6469,6471,6473,6475],{"class":89,"line":189},[87,6452,1279],{"class":148},[87,6454,1282],{"class":93},[87,6456,219],{"class":148},[87,6458,1287],{"class":415},[87,6460,354],{"class":148},[87,6462,987],{"class":93},[87,6464,348],{"class":148},[87,6466,1016],{"class":93},[87,6468,1019],{"class":148},[87,6470,1300],{"class":97},[87,6472,1025],{"class":148},[87,6474,1305],{"class":97},[87,6476,422],{"class":148},[87,6478,6479],{"class":89,"line":201},[87,6480,1312],{"class":126},[87,6482,6483],{"class":89,"line":211},[87,6484,1317],{"class":148},[87,6486,6487],{"class":89,"line":228},[87,6488,142],{"emptyLinePlaceholder":141},[87,6490,6491,6493,6495,6497,6499,6501,6503,6505,6507],{"class":89,"line":390},[87,6492,1326],{"class":148},[87,6494,152],{"class":126},[87,6496,1331],{"class":148},[87,6498,1334],{"class":93},[87,6500,1337],{"class":148},[87,6502,1340],{"class":97},[87,6504,354],{"class":148},[87,6506,1345],{"class":415},[87,6508,231],{"class":148},[87,6510,6511,6513,6515,6517,6519,6521,6523,6525,6527,6529,6531,6533,6535,6537],{"class":89,"line":396},[87,6512,1263],{"class":126},[87,6514,1354],{"class":93},[87,6516,1357],{"class":148},[87,6518,589],{"class":126},[87,6520,1362],{"class":415},[87,6522,1365],{"class":126},[87,6524,1368],{"class":126},[87,6526,1371],{"class":148},[87,6528,1374],{"class":93},[87,6530,1377],{"class":148},[87,6532,1380],{"class":415},[87,6534,1383],{"class":148},[87,6536,1386],{"class":97},[87,6538,387],{"class":148},[87,6540,6541,6543,6545,6547,6549,6551,6553,6555,6557,6559,6561,6563,6565],{"class":89,"line":419},[87,6542,1279],{"class":148},[87,6544,1282],{"class":93},[87,6546,219],{"class":148},[87,6548,1287],{"class":415},[87,6550,354],{"class":148},[87,6552,987],{"class":93},[87,6554,348],{"class":148},[87,6556,1016],{"class":93},[87,6558,1019],{"class":148},[87,6560,1300],{"class":97},[87,6562,1025],{"class":148},[87,6564,1415],{"class":97},[87,6566,422],{"class":148},[87,6568,6569],{"class":89,"line":617},[87,6570,1312],{"class":126},[87,6572,6573],{"class":89,"line":623},[87,6574,1317],{"class":148},[87,6576,6577],{"class":89,"line":629},[87,6578,142],{"emptyLinePlaceholder":141},[87,6580,6581,6583,6585,6587,6589,6591,6593],{"class":89,"line":647},[87,6582,1434],{"class":148},[87,6584,152],{"class":126},[87,6586,1439],{"class":148},[87,6588,1442],{"class":93},[87,6590,1377],{"class":148},[87,6592,1447],{"class":415},[87,6594,1450],{"class":148},[87,6596,6597,6599,6601,6603,6605],{"class":89,"line":927},[87,6598,1263],{"class":126},[87,6600,586],{"class":148},[87,6602,589],{"class":126},[87,6604,592],{"class":415},[87,6606,595],{"class":148},[87,6608,6609,6611,6613,6615,6617,6619,6621,6623,6625,6627,6629,6631,6633],{"class":89,"line":932},[87,6610,1279],{"class":148},[87,6612,1282],{"class":93},[87,6614,219],{"class":148},[87,6616,1287],{"class":415},[87,6618,354],{"class":148},[87,6620,987],{"class":93},[87,6622,348],{"class":148},[87,6624,1016],{"class":93},[87,6626,1019],{"class":148},[87,6628,1300],{"class":97},[87,6630,1025],{"class":148},[87,6632,1489],{"class":97},[87,6634,422],{"class":148},[87,6636,6637],{"class":89,"line":951},[87,6638,1312],{"class":126},[87,6640,6641],{"class":89,"line":956},[87,6642,1317],{"class":148},[87,6644,6645],{"class":89,"line":962},[87,6646,142],{"emptyLinePlaceholder":141},[87,6648,6649,6651,6653,6655,6657],{"class":89,"line":996},[87,6650,999],{"class":148},[87,6652,1510],{"class":93},[87,6654,219],{"class":148},[87,6656,1515],{"class":97},[87,6658,1518],{"class":148},[87,6660,6661,6663,6665,6667,6669],{"class":89,"line":1033},[87,6662,999],{"class":148},[87,6664,1510],{"class":93},[87,6666,219],{"class":148},[87,6668,1529],{"class":97},[87,6670,1532],{"class":148},[87,6672,6673,6675,6677],{"class":89,"line":1039},[87,6674,999],{"class":148},[87,6676,1539],{"class":93},[87,6678,805],{"class":148},[87,6680,6681],{"class":89,"line":1044},[87,6682,620],{"class":148},[87,6684,6685],{"class":89,"line":1059},[87,6686,1062],{"class":148},[78,6688,6689],{"className":118,"code":1552,"language":94,"meta":83,"style":83},[30,6690,6691,6695,6703,6709,6719,6729,6733,6737,6753,6773,6785,6805,6825,6837,6845,6849],{"__ignoreMap":83},[87,6692,6693],{"class":89,"line":90},[87,6694,1559],{"class":173},[87,6696,6697,6699,6701],{"class":89,"line":104},[87,6698,1564],{"class":126},[87,6700,1567],{"class":93},[87,6702,1570],{"class":126},[87,6704,6705,6707],{"class":89,"line":145},[87,6706,1575],{"class":126},[87,6708,1578],{"class":148},[87,6710,6711,6713,6715,6717],{"class":89,"line":164},[87,6712,1583],{"class":415},[87,6714,1586],{"class":93},[87,6716,1589],{"class":126},[87,6718,1592],{"class":97},[87,6720,6721,6723,6725,6727],{"class":89,"line":177},[87,6722,1597],{"class":415},[87,6724,1567],{"class":93},[87,6726,1589],{"class":126},[87,6728,1604],{"class":97},[87,6730,6731],{"class":89,"line":189},[87,6732,231],{"class":148},[87,6734,6735],{"class":89,"line":201},[87,6736,142],{"emptyLinePlaceholder":141},[87,6738,6739,6741,6743,6745,6747,6749,6751],{"class":89,"line":211},[87,6740,336],{"class":126},[87,6742,1619],{"class":93},[87,6744,1622],{"class":148},[87,6746,987],{"class":93},[87,6748,348],{"class":148},[87,6750,1215],{"class":93},[87,6752,595],{"class":148},[87,6754,6755,6757,6759,6761,6763,6765,6767,6769,6771],{"class":89,"line":228},[87,6756,399],{"class":126},[87,6758,1224],{"class":126},[87,6760,219],{"class":148},[87,6762,981],{"class":341},[87,6764,984],{"class":126},[87,6766,987],{"class":93},[87,6768,348],{"class":148},[87,6770,351],{"class":93},[87,6772,387],{"class":148},[87,6774,6775,6777,6779,6781,6783],{"class":89,"line":390},[87,6776,1655],{"class":148},[87,6778,152],{"class":126},[87,6780,1660],{"class":148},[87,6782,351],{"class":93},[87,6784,805],{"class":148},[87,6786,6787,6789,6791,6793,6795,6797,6799,6801,6803],{"class":89,"line":396},[87,6788,1655],{"class":148},[87,6790,1671],{"class":126},[87,6792,1674],{"class":148},[87,6794,1677],{"class":93},[87,6796,1680],{"class":148},[87,6798,1683],{"class":93},[87,6800,219],{"class":148},[87,6802,1515],{"class":97},[87,6804,1690],{"class":148},[87,6806,6807,6809,6811,6813,6815,6817,6819,6821,6823],{"class":89,"line":419},[87,6808,1655],{"class":148},[87,6810,1671],{"class":126},[87,6812,1674],{"class":148},[87,6814,1677],{"class":93},[87,6816,1703],{"class":148},[87,6818,1706],{"class":93},[87,6820,219],{"class":148},[87,6822,1529],{"class":97},[87,6824,1690],{"class":148},[87,6826,6827,6829,6831,6833,6835],{"class":89,"line":617},[87,6828,1717],{"class":148},[87,6830,1671],{"class":126},[87,6832,1660],{"class":148},[87,6834,1724],{"class":93},[87,6836,1727],{"class":148},[87,6838,6839,6841,6843],{"class":89,"line":623},[87,6840,999],{"class":148},[87,6842,1539],{"class":93},[87,6844,805],{"class":148},[87,6846,6847],{"class":89,"line":629},[87,6848,620],{"class":148},[87,6850,6851],{"class":89,"line":647},[87,6852,1062],{"class":148},[73,6854,1746],{"id":1746},[78,6856,6857],{"className":118,"code":1749,"language":94,"meta":83,"style":83},[30,6858,6859,6871,6879,6887,6897],{"__ignoreMap":83},[87,6860,6861,6863,6865,6867,6869],{"class":89,"line":90},[87,6862,747],{"class":148},[87,6864,750],{"class":93},[87,6866,219],{"class":148},[87,6868,710],{"class":97},[87,6870,294],{"class":148},[87,6872,6873,6875,6877],{"class":89,"line":104},[87,6874,1768],{"class":93},[87,6876,1771],{"class":148},[87,6878,1774],{"class":173},[87,6880,6881,6883,6885],{"class":89,"line":145},[87,6882,1779],{"class":93},[87,6884,1782],{"class":148},[87,6886,1785],{"class":173},[87,6888,6889,6891,6893,6895],{"class":89,"line":164},[87,6890,1790],{"class":148},[87,6892,760],{"class":93},[87,6894,1795],{"class":148},[87,6896,1798],{"class":173},[87,6898,6899],{"class":89,"line":177},[87,6900,231],{"class":148},[16,6902,1805],{},[78,6904,6905],{"className":118,"code":1808,"language":94,"meta":83,"style":83},[30,6906,6907,6959,6975,6983,6999,7003,7007,7019,7023,7035],{"__ignoreMap":83},[87,6908,6909,6911,6913,6915,6917,6919,6921,6923,6925,6927,6929,6931,6933,6935,6937,6939,6941,6943,6945,6947,6949,6951,6953,6955,6957],{"class":89,"line":90},[87,6910,336],{"class":126},[87,6912,1817],{"class":148},[87,6914,1820],{"class":341},[87,6916,371],{"class":126},[87,6918,1825],{"class":93},[87,6920,1208],{"class":148},[87,6922,1830],{"class":93},[87,6924,219],{"class":148},[87,6926,342],{"class":341},[87,6928,345],{"class":93},[87,6930,348],{"class":148},[87,6932,351],{"class":93},[87,6934,354],{"class":148},[87,6936,357],{"class":341},[87,6938,360],{"class":93},[87,6940,348],{"class":148},[87,6942,365],{"class":93},[87,6944,368],{"class":148},[87,6946,371],{"class":126},[87,6948,374],{"class":93},[87,6950,348],{"class":148},[87,6952,379],{"class":93},[87,6954,354],{"class":148},[87,6956,384],{"class":126},[87,6958,387],{"class":148},[87,6960,6961,6963,6965,6967,6969,6971,6973],{"class":89,"line":104},[87,6962,1871],{"class":148},[87,6964,152],{"class":126},[87,6966,1876],{"class":148},[87,6968,1879],{"class":93},[87,6970,1882],{"class":148},[87,6972,1885],{"class":126},[87,6974,231],{"class":148},[87,6976,6977,6979,6981],{"class":89,"line":145},[87,6978,583],{"class":126},[87,6980,1368],{"class":126},[87,6982,1896],{"class":148},[87,6984,6985,6987,6989,6991,6993,6995,6997],{"class":89,"line":164},[87,6986,600],{"class":126},[87,6988,283],{"class":148},[87,6990,605],{"class":93},[87,6992,219],{"class":148},[87,6994,1909],{"class":97},[87,6996,412],{"class":148},[87,6998,416],{"class":415},[87,7000,7001],{"class":89,"line":177},[87,7002,620],{"class":148},[87,7004,7005],{"class":89,"line":189},[87,7006,142],{"emptyLinePlaceholder":141},[87,7008,7009,7011,7013,7015,7017],{"class":89,"line":201},[87,7010,1926],{"class":148},[87,7012,152],{"class":126},[87,7014,1931],{"class":148},[87,7016,1934],{"class":93},[87,7018,1937],{"class":148},[87,7020,7021],{"class":89,"line":211},[87,7022,1942],{"class":173},[87,7024,7025,7027,7029,7031,7033],{"class":89,"line":228},[87,7026,399],{"class":126},[87,7028,283],{"class":148},[87,7030,404],{"class":93},[87,7032,1953],{"class":148},[87,7034,416],{"class":415},[87,7036,7037],{"class":89,"line":390},[87,7038,1062],{"class":148},[73,7040,1962],{"id":1962},[16,7042,1965],{},[78,7044,7045],{"className":1968,"code":1969,"language":1970,"meta":83,"style":83},[30,7046,7047,7051,7057,7063,7073,7079,7087,7091,7095,7099],{"__ignoreMap":83},[87,7048,7049],{"class":89,"line":90},[87,7050,1977],{"class":148},[87,7052,7053,7055],{"class":89,"line":104},[87,7054,1982],{"class":415},[87,7056,1985],{"class":148},[87,7058,7059,7061],{"class":89,"line":145},[87,7060,167],{"class":415},[87,7062,1985],{"class":148},[87,7064,7065,7067,7069,7071],{"class":89,"line":164},[87,7066,1996],{"class":415},[87,7068,1025],{"class":148},[87,7070,2001],{"class":97},[87,7072,294],{"class":148},[87,7074,7075,7077],{"class":89,"line":177},[87,7076,2008],{"class":415},[87,7078,1985],{"class":148},[87,7080,7081,7083,7085],{"class":89,"line":189},[87,7082,2015],{"class":415},[87,7084,1025],{"class":148},[87,7086,2020],{"class":97},[87,7088,7089],{"class":89,"line":201},[87,7090,2025],{"class":148},[87,7092,7093],{"class":89,"line":211},[87,7094,620],{"class":148},[87,7096,7097],{"class":89,"line":228},[87,7098,2034],{"class":148},[87,7100,7101],{"class":89,"line":390},[87,7102,1062],{"class":148},[11,7104,2041],{"id":2041},[2043,7106,7107,7115],{},[2046,7108,7109],{},[2049,7110,7111,7113],{},[2052,7112,2054],{},[2052,7114,2057],{},[2059,7116,7117,7125,7135,7143,7151],{},[2049,7118,7119,7123],{},[2064,7120,7121],{},[30,7122,158],{},[2064,7124,2070],{},[2049,7126,7127,7133],{},[2064,7128,7129,2077,7131],{},[30,7130,286],{},[30,7132,330],{},[2064,7134,2082],{},[2049,7136,7137,7141],{},[2064,7138,7139],{},[30,7140,695],{},[2064,7142,2091],{},[2049,7144,7145,7149],{},[2064,7146,7147],{},[30,7148,663],{},[2064,7150,2100],{},[2049,7152,7153,7159],{},[2064,7154,7155,2077,7157],{},[30,7156,2107],{},[30,7158,2110],{},[2064,7160,2113],{},[16,7162,2116,7163,2120,7165,1178],{},[38,7164,2119],{},[30,7166,1177],{},[2124,7168,2126],{},{"title":83,"searchDepth":104,"depth":145,"links":7170},[7171,7172,7173,7174,7181,7187],{"id":13,"depth":104,"text":14},{"id":24,"depth":104,"text":25},{"id":43,"depth":104,"text":43},{"id":71,"depth":104,"text":71,"children":7175},[7176,7177,7178,7179,7180],{"id":75,"depth":145,"text":76},{"id":114,"depth":145,"text":115},{"id":247,"depth":145,"text":248},{"id":652,"depth":145,"text":653},{"id":773,"depth":145,"text":774},{"id":1149,"depth":104,"text":1150,"children":7182},[7183,7184,7185,7186],{"id":1156,"depth":145,"text":1156},{"id":1170,"depth":145,"text":1171},{"id":1746,"depth":145,"text":1746},{"id":1962,"depth":145,"text":1962},{"id":2041,"depth":104,"text":2041},{},{"title":5,"description":2146},[2158,2159,2160,2161,2162],{"id":7192,"title":7193,"author":6,"body":7194,"description":7961,"difficulty":6,"draft":2148,"extension":2149,"image":7962,"meta":7963,"navigation":141,"path":7964,"pinned":2148,"published":7965,"seo":7966,"series":6,"seriesOrder":6,"stem":7967,"tags":7968,"updated":6,"__hash__":7972},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":8,"value":7195,"toc":7947},[7196,7200,7203,7210,7336,7339,7342,7398,7401,7416,7419,7422,7429,7661,7668,7672,7675,7695,7698,7713,7716,7736,7739,7742,7749,7815,7818,7821,7825,7828,7880,7884,7905,7909,7912,7926,7929,7933,7936,7938,7941,7944],[11,7197,7199],{"id":7198},"dws-是什么","dws 是什么",[16,7201,7202],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[16,7204,7205,7206,7209],{},"它的定位很明确：",[38,7207,7208],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[78,7211,7213],{"className":80,"code":7212,"language":82,"meta":83,"style":83},"# 查看今天有什么会\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",[30,7214,7215,7220,7246,7250,7255,7277,7281,7286,7311,7315,7320],{"__ignoreMap":83},[87,7216,7217],{"class":89,"line":90},[87,7218,7219],{"class":173},"# 查看今天有什么会\n",[87,7221,7222,7225,7228,7231,7234,7237,7240,7243],{"class":89,"line":104},[87,7223,7224],{"class":93},"dws",[87,7226,7227],{"class":97}," calendar",[87,7229,7230],{"class":97}," event",[87,7232,7233],{"class":97}," list",[87,7235,7236],{"class":415}," --start",[87,7238,7239],{"class":97}," 2026-06-01",[87,7241,7242],{"class":415}," --end",[87,7244,7245],{"class":97}," 2026-06-01\n",[87,7247,7248],{"class":89,"line":145},[87,7249,142],{"emptyLinePlaceholder":141},[87,7251,7252],{"class":89,"line":164},[87,7253,7254],{"class":173},"# 给群里发条消息\n",[87,7256,7257,7259,7262,7265,7268,7271,7274],{"class":89,"line":177},[87,7258,7224],{"class":93},[87,7260,7261],{"class":97}," chat",[87,7263,7264],{"class":97}," send",[87,7266,7267],{"class":415}," --group-id",[87,7269,7270],{"class":97}," xxx",[87,7272,7273],{"class":415}," --content",[87,7275,7276],{"class":97}," \"今天发布推迟到下午3点\"\n",[87,7278,7279],{"class":89,"line":189},[87,7280,142],{"emptyLinePlaceholder":141},[87,7282,7283],{"class":89,"line":201},[87,7284,7285],{"class":173},"# 创建一个待办\n",[87,7287,7288,7290,7293,7296,7299,7302,7305,7308],{"class":89,"line":211},[87,7289,7224],{"class":93},[87,7291,7292],{"class":97}," todo",[87,7294,7295],{"class":97}," task",[87,7297,7298],{"class":97}," create",[87,7300,7301],{"class":415}," --subject",[87,7303,7304],{"class":97}," \"Review PR #123\"",[87,7306,7307],{"class":415}," --due-date",[87,7309,7310],{"class":97}," 2026-06-02\n",[87,7312,7313],{"class":89,"line":228},[87,7314,142],{"emptyLinePlaceholder":141},[87,7316,7317],{"class":89,"line":390},[87,7318,7319],{"class":173},"# 读取钉钉文档\n",[87,7321,7322,7324,7327,7330,7333],{"class":89,"line":396},[87,7323,7224],{"class":93},[87,7325,7326],{"class":97}," doc",[87,7328,7329],{"class":97}," read",[87,7331,7332],{"class":415}," --url",[87,7334,7335],{"class":97}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[11,7337,7338],{"id":7338},"安装",[16,7340,7341],{},"三种方式，选你喜欢的：",[78,7343,7345],{"className":80,"code":7344,"language":82,"meta":83,"style":83},"# 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",[30,7346,7347,7352,7365,7369,7374,7384,7388,7393],{"__ignoreMap":83},[87,7348,7349],{"class":89,"line":90},[87,7350,7351],{"class":173},"# npm（推荐，跨平台自动下载二进制）\n",[87,7353,7354,7357,7359,7362],{"class":89,"line":104},[87,7355,7356],{"class":93},"npm",[87,7358,2251],{"class":97},[87,7360,7361],{"class":415}," -g",[87,7363,7364],{"class":97}," dingtalk-workspace-cli\n",[87,7366,7367],{"class":89,"line":145},[87,7368,142],{"emptyLinePlaceholder":141},[87,7370,7371],{"class":89,"line":164},[87,7372,7373],{"class":173},"# Homebrew（macOS\u002FLinux）\n",[87,7375,7376,7379,7381],{"class":89,"line":177},[87,7377,7378],{"class":93},"brew",[87,7380,2251],{"class":97},[87,7382,7383],{"class":97}," dws\n",[87,7385,7386],{"class":89,"line":189},[87,7387,142],{"emptyLinePlaceholder":141},[87,7389,7390],{"class":89,"line":201},[87,7391,7392],{"class":173},"# 直接下载二进制\n",[87,7394,7395],{"class":89,"line":211},[87,7396,7397],{"class":173},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[16,7399,7400],{},"安装后认证：",[78,7402,7404],{"className":80,"code":7403,"language":82,"meta":83,"style":83},"dws auth login\n",[30,7405,7406],{"__ignoreMap":83},[87,7407,7408,7410,7413],{"class":89,"line":90},[87,7409,7224],{"class":93},[87,7411,7412],{"class":97}," auth",[87,7414,7415],{"class":97}," login\n",[16,7417,7418],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[11,7420,7421],{"id":7421},"覆盖的钉钉产品",[16,7423,7424,7425,7428],{},"这是最夸张的部分 — ",[38,7426,7427],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[2043,7430,7431,7443],{},[2046,7432,7433],{},[2049,7434,7435,7438,7441],{},[2052,7436,7437],{},"产品",[2052,7439,7440],{},"命令数",[2052,7442,2057],{},[2059,7444,7445,7459,7473,7487,7501,7515,7529,7543,7556,7570,7583,7597,7611,7624,7637,7650],{},[2049,7446,7447,7453,7456],{},[2064,7448,7449,7450],{},"聊天 ",[30,7451,7452],{},"chat",[2064,7454,7455],{},"57",[2064,7457,7458],{},"消息收发、群管理、机器人、已读状态、Webhook",[2049,7460,7461,7467,7470],{},[2064,7462,7463,7464],{},"AI 表格 ",[30,7465,7466],{},"aitable",[2064,7468,7469],{},"42",[2064,7471,7472],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[2049,7474,7475,7481,7484],{},[2064,7476,7477,7478],{},"电子表格 ",[30,7479,7480],{},"sheet",[2064,7482,7483],{},"34",[2064,7485,7486],{},"工作表读写、行列操作、合并、筛选视图、导出",[2049,7488,7489,7495,7498],{},[2064,7490,7491,7492],{},"文档 ",[30,7493,7494],{},"doc",[2064,7496,7497],{},"21",[2064,7499,7500],{},"搜索、读写、块级编辑、评论、复制移动",[2049,7502,7503,7509,7512],{},[2064,7504,7505,7506],{},"会议纪要 ",[30,7507,7508],{},"minutes",[2064,7510,7511],{},"19",[2064,7513,7514],{},"列表、摘要、转写、思维导图、发言人、热词",[2049,7516,7517,7523,7526],{},[2064,7518,7519,7520],{},"日历 ",[30,7521,7522],{},"calendar",[2064,7524,7525],{},"14",[2064,7527,7528],{},"事件 CRUD、会议室、忙闲查询、时间建议",[2049,7530,7531,7537,7540],{},[2064,7532,7533,7534],{},"审批 ",[30,7535,7536],{},"oa",[2064,7538,7539],{},"9",[2064,7541,7542],{},"待办、发起、同意\u002F拒绝\u002F撤销",[2049,7544,7545,7551,7553],{},[2064,7546,7547,7548],{},"云盘 ",[30,7549,7550],{},"drive",[2064,7552,7539],{},[2064,7554,7555],{},"列表、下载、上传、创建目录",[2049,7557,7558,7564,7567],{},[2064,7559,7560,7561],{},"知识库 ",[30,7562,7563],{},"wiki",[2064,7565,7566],{},"7",[2064,7568,7569],{},"空间管理、成员管理",[2049,7571,7572,7578,7580],{},[2064,7573,7574,7575],{},"汇报 ",[30,7576,7577],{},"report",[2064,7579,7566],{},[2064,7581,7582],{},"日志\u002F日报\u002F周报，按模板创建、统计",[2049,7584,7585,7591,7594],{},[2064,7586,7587,7588],{},"待办 ",[30,7589,7590],{},"todo",[2064,7592,7593],{},"6",[2064,7595,7596],{},"创建、完成、删除，支持优先级和循环",[2049,7598,7599,7605,7608],{},[2064,7600,7601,7602],{},"邮件 ",[30,7603,7604],{},"mail",[2064,7606,7607],{},"4",[2064,7609,7610],{},"邮箱查询、邮件搜索、发送",[2049,7612,7613,7619,7621],{},[2064,7614,7615,7616],{},"考勤 ",[30,7617,7618],{},"attendance",[2064,7620,7607],{},[2064,7622,7623],{},"打卡记录、排班、汇总",[2049,7625,7626,7632,7634],{},[2064,7627,7628,7629],{},"通讯录 ",[30,7630,7631],{},"contact",[2064,7633,4101],{},[2064,7635,7636],{},"用户搜索、部门查询",[2049,7638,7639,7645,7647],{},[2064,7640,7641,7642],{},"DING 消息 ",[30,7643,7644],{},"ding",[2064,7646,1345],{},[2064,7648,7649],{},"发送\u002F撤回（应用内、短信、电话）",[2049,7651,7652,7655,7658],{},[2064,7653,7654],{},"其他",[2064,7656,7657],{},"若干",[2064,7659,7660],{},"AI 搜索、AI 应用、直播、开放平台文档",[16,7662,7663,7664,7667],{},"还有一个万能命令 ",[30,7665,7666],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[11,7669,7671],{"id":7670},"agent-skill-系统","Agent Skill 系统",[16,7673,7674],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[3705,7676,7677,7683,7689],{},[51,7678,7679,7682],{},[38,7680,7681],{},"Claude Code"," — 通过 SKILL.md 自动加载",[51,7684,7685,7688],{},[38,7686,7687],{},"Cursor"," — 支持 MCP 协议接入",[51,7690,7691,7694],{},[38,7692,7693],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[16,7696,7697],{},"安装 skill：",[78,7699,7701],{"className":80,"code":7700,"language":82,"meta":83,"style":83},"dws skill setup\n",[30,7702,7703],{"__ignoreMap":83},[87,7704,7705,7707,7710],{"class":89,"line":90},[87,7706,7224],{"class":93},[87,7708,7709],{"class":97}," skill",[87,7711,7712],{"class":97}," setup\n",[16,7714,7715],{},"SKILL.md 里定义了：",[3705,7717,7718,7727,7730,7733],{},[51,7719,7720,7721,7723,7724,7726],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[30,7722,7466],{},"，说\"打卡\"→ ",[30,7725,7618],{},"）",[51,7728,7729],{},"危险操作确认流程（删除、撤回等必须先确认）",[51,7731,7732],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[51,7734,7735],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[16,7737,7738],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[11,7740,7741],{"id":7741},"自动升级",[16,7743,7744,7745,7748],{},"内置 ",[30,7746,7747],{},"dws upgrade","，不需要重新下载安装包：",[78,7750,7752],{"className":80,"code":7751,"language":82,"meta":83,"style":83},"dws upgrade              # 交互式升级到最新版\ndws upgrade --check      # 仅检查是否有新版本\ndws upgrade --list       # 列出最近版本\ndws upgrade --version v1.0.5  # 升级到指定版本\ndws upgrade --rollback   # 回滚到上一版本\n",[30,7753,7754,7764,7776,7788,7803],{"__ignoreMap":83},[87,7755,7756,7758,7761],{"class":89,"line":90},[87,7757,7224],{"class":93},[87,7759,7760],{"class":97}," upgrade",[87,7762,7763],{"class":173},"              # 交互式升级到最新版\n",[87,7765,7766,7768,7770,7773],{"class":89,"line":104},[87,7767,7224],{"class":93},[87,7769,7760],{"class":97},[87,7771,7772],{"class":415}," --check",[87,7774,7775],{"class":173},"      # 仅检查是否有新版本\n",[87,7777,7778,7780,7782,7785],{"class":89,"line":145},[87,7779,7224],{"class":93},[87,7781,7760],{"class":97},[87,7783,7784],{"class":415}," --list",[87,7786,7787],{"class":173},"       # 列出最近版本\n",[87,7789,7790,7792,7794,7797,7800],{"class":89,"line":164},[87,7791,7224],{"class":93},[87,7793,7760],{"class":97},[87,7795,7796],{"class":415}," --version",[87,7798,7799],{"class":97}," v1.0.5",[87,7801,7802],{"class":173},"  # 升级到指定版本\n",[87,7804,7805,7807,7809,7812],{"class":89,"line":177},[87,7806,7224],{"class":93},[87,7808,7760],{"class":97},[87,7810,7811],{"class":415}," --rollback",[87,7813,7814],{"class":173},"   # 回滚到上一版本\n",[16,7816,7817],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[11,7819,7820],{"id":7820},"亮点",[73,7822,7824],{"id":7823},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[16,7826,7827],{},"这可能是目前对 AI 最友好的 CLI 工具：",[3705,7829,7830,7851,7861,7870],{},[51,7831,7832,7835,7836,7839,7840,7843,7844,7839,7847,7850],{},[38,7833,7834],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[30,7837,7838],{},"--userId"," 应该是 ",[30,7841,7842],{},"--user-id","，",[30,7845,7846],{},"--limit100",[30,7848,7849],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[51,7852,7853,7856,7857,7860],{},[38,7854,7855],{},"Schema 发现"," — ",[30,7858,7859],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[51,7862,7863,7856,7866,7869],{},[38,7864,7865],{},"jq 过滤",[30,7867,7868],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[51,7871,7872,7875,7876,7879],{},[38,7873,7874],{},"JSON 输出"," — 所有命令支持 ",[30,7877,7878],{},"--format json","，方便程序解析",[73,7881,7883],{"id":7882},"_2-安全设计","2. 安全设计",[3705,7885,7886,7889,7892,7895,7902],{},[51,7887,7888],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[51,7890,7891],{},"域名白名单，只能访问钉钉官方 API",[51,7893,7894],{},"强制 HTTPS",[51,7896,7897,7898,7901],{},"写操作必须显式 ",[30,7899,7900],{},"--yes"," 确认",[51,7903,7904],{},"完整审计链",[73,7906,7908],{"id":7907},"_3-发现驱动架构","3. 发现驱动架构",[16,7910,7911],{},"不是把所有命令硬编码在代码里，而是：",[48,7913,7914,7917,7920,7923],{},[51,7915,7916],{},"从钉钉 MCP 服务器注册中心拉取元数据",[51,7918,7919],{},"运行时发现服务端能力",[51,7921,7922],{},"转换成统一的中间表示（IR）",[51,7924,7925],{},"动态挂载到命令树",[16,7927,7928],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[73,7930,7932],{"id":7931},"_4-多平台分发","4. 多平台分发",[16,7934,7935],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[11,7937,2041],{"id":2041},[16,7939,7940],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[16,7942,7943],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[2124,7945,7946],{},"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":83,"searchDepth":104,"depth":145,"links":7948},[7949,7950,7951,7952,7953,7954,7960],{"id":7198,"depth":104,"text":7199},{"id":7338,"depth":104,"text":7338},{"id":7421,"depth":104,"text":7421},{"id":7670,"depth":104,"text":7671},{"id":7741,"depth":104,"text":7741},{"id":7820,"depth":104,"text":7820,"children":7955},[7956,7957,7958,7959],{"id":7823,"depth":145,"text":7824},{"id":7882,"depth":145,"text":7883},{"id":7907,"depth":145,"text":7908},{"id":7931,"depth":145,"text":7932},{"id":2041,"depth":104,"text":2041},"介绍阿里巴巴官方的 DingTalk Workspace CLI（dws），一个覆盖钉钉 19 个产品、213 个命令的 AI 友好命令行工具，支持自动升级和 Agent Skill 系统。","\u002Fposts\u002Fdws-dingtalk-cli\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdws-dingtalk-cli","2026-06-01T00:51:00",{"title":7193,"description":7961},"posts\u002Fdws-dingtalk-cli\u002Findex",[7969,7970,2161,2158,7971],"钉钉","CLI","效率工具","4ca65VUZ2Sz9yG8dNtftrBDDHE6QowA3ed9g4egQkKI",{"id":7974,"title":7975,"author":6,"body":7976,"description":8939,"difficulty":2147,"draft":2148,"extension":2149,"image":8940,"meta":8941,"navigation":141,"path":8942,"pinned":2148,"published":8943,"seo":8944,"series":2155,"seriesOrder":104,"stem":8945,"tags":8946,"updated":8950,"__hash__":8951},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",{"type":8,"value":7977,"toc":8922},[7978,7980,7987,7990,7993,8004,8007,8011,8344,8348,8456,8460,8594,8597,8601,8610,8627,8638,8657,8671,8677,8685,8784,8790,8794,8801,8810,8813,8849,8852,8857,8896,8898,8904,8916,8919],[11,7979,4021],{"id":4021},[16,7981,7982,7983,7986],{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[30,7984,7985],{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[16,7988,7989],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[11,7991,7992],{"id":7992},"核心思路",[16,7994,7995,7996,7999,8000,8003],{},"利用 Go 1.16 引入的 ",[30,7997,7998],{},"\u002F\u002Fgo:embed"," 指令，在编译时将前端 ",[30,8001,8002],{},"dist\u002F"," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[11,8005,8006],{"id":8006},"逐步实现",[73,8008,8010],{"id":8009},"_1-创建嵌入模块","1. 创建嵌入模块",[78,8012,8014],{"className":118,"code":8013,"language":94,"meta":83,"style":83},"\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",[30,8015,8016,8021,8029,8033,8039,8049,8058,8067,8076,8085,8094,8098,8107,8111,8115,8120,8135,8139,8156,8177,8181,8201,8221,8234,8244,8248,8252,8267,8279,8284,8303,8307,8311,8334,8339],{"__ignoreMap":83},[87,8017,8018],{"class":89,"line":90},[87,8019,8020],{"class":173},"\u002F\u002F service\u002Fweb\u002Fembed.go\n",[87,8022,8023,8026],{"class":89,"line":104},[87,8024,8025],{"class":126},"package",[87,8027,8028],{"class":93}," web\n",[87,8030,8031],{"class":89,"line":145},[87,8032,142],{"emptyLinePlaceholder":141},[87,8034,8035,8037],{"class":89,"line":164},[87,8036,127],{"class":126},[87,8038,1578],{"class":148},[87,8040,8041,8044,8047],{"class":89,"line":177},[87,8042,8043],{"class":97},"    \"",[87,8045,8046],{"class":93},"embed",[87,8048,136],{"class":97},[87,8050,8051,8053,8056],{"class":89,"line":189},[87,8052,8043],{"class":97},[87,8054,8055],{"class":93},"io\u002Ffs",[87,8057,136],{"class":97},[87,8059,8060,8062,8065],{"class":89,"line":201},[87,8061,8043],{"class":97},[87,8063,8064],{"class":93},"mime",[87,8066,136],{"class":97},[87,8068,8069,8071,8074],{"class":89,"line":211},[87,8070,8043],{"class":97},[87,8072,8073],{"class":93},"net\u002Fhttp",[87,8075,136],{"class":97},[87,8077,8078,8080,8083],{"class":89,"line":228},[87,8079,8043],{"class":97},[87,8081,8082],{"class":93},"path\u002Ffilepath",[87,8084,136],{"class":97},[87,8086,8087,8089,8092],{"class":89,"line":390},[87,8088,8043],{"class":97},[87,8090,8091],{"class":93},"strings",[87,8093,136],{"class":97},[87,8095,8096],{"class":89,"line":396},[87,8097,142],{"emptyLinePlaceholder":141},[87,8099,8100,8102,8105],{"class":89,"line":419},[87,8101,8043],{"class":97},[87,8103,8104],{"class":93},"github.com\u002Fgin-gonic\u002Fgin",[87,8106,136],{"class":97},[87,8108,8109],{"class":89,"line":617},[87,8110,231],{"class":148},[87,8112,8113],{"class":89,"line":623},[87,8114,142],{"emptyLinePlaceholder":141},[87,8116,8117],{"class":89,"line":629},[87,8118,8119],{"class":173},"\u002F\u002Fgo:embed dist\n",[87,8121,8122,8125,8128,8130,8132],{"class":89,"line":647},[87,8123,8124],{"class":126},"var",[87,8126,8127],{"class":148}," embedFS ",[87,8129,8046],{"class":93},[87,8131,348],{"class":148},[87,8133,8134],{"class":93},"FS\n",[87,8136,8137],{"class":89,"line":927},[87,8138,142],{"emptyLinePlaceholder":141},[87,8140,8141,8143,8146,8148,8150,8152,8154],{"class":89,"line":932},[87,8142,336],{"class":126},[87,8144,8145],{"class":93}," SPAHandler",[87,8147,1622],{"class":148},[87,8149,987],{"class":93},[87,8151,348],{"class":148},[87,8153,1215],{"class":93},[87,8155,595],{"class":148},[87,8157,8158,8161,8163,8166,8169,8172,8175],{"class":89,"line":951},[87,8159,8160],{"class":148},"    sub, _ ",[87,8162,152],{"class":126},[87,8164,8165],{"class":148}," fs.",[87,8167,8168],{"class":93},"Sub",[87,8170,8171],{"class":148},"(embedFS, ",[87,8173,8174],{"class":97},"\"dist\"",[87,8176,231],{"class":148},[87,8178,8179],{"class":89,"line":956},[87,8180,142],{"emptyLinePlaceholder":141},[87,8182,8183,8185,8187,8189,8191,8193,8195,8197,8199],{"class":89,"line":962},[87,8184,399],{"class":126},[87,8186,1224],{"class":126},[87,8188,219],{"class":148},[87,8190,981],{"class":341},[87,8192,984],{"class":126},[87,8194,987],{"class":93},[87,8196,348],{"class":148},[87,8198,351],{"class":93},[87,8200,387],{"class":148},[87,8202,8203,8206,8208,8210,8213,8216,8219],{"class":89,"line":996},[87,8204,8205],{"class":148},"        p ",[87,8207,152],{"class":126},[87,8209,1331],{"class":148},[87,8211,8212],{"class":93},"TrimPrefix",[87,8214,8215],{"class":148},"(c.Request.URL.Path, ",[87,8217,8218],{"class":97},"\"\u002F\"",[87,8220,231],{"class":148},[87,8222,8223,8225,8228,8230,8232],{"class":89,"line":1033},[87,8224,1263],{"class":126},[87,8226,8227],{"class":148}," p ",[87,8229,1269],{"class":126},[87,8231,1272],{"class":97},[87,8233,595],{"class":148},[87,8235,8236,8239,8241],{"class":89,"line":1039},[87,8237,8238],{"class":148},"            p ",[87,8240,1671],{"class":126},[87,8242,8243],{"class":97}," \"index.html\"\n",[87,8245,8246],{"class":89,"line":1044},[87,8247,1317],{"class":148},[87,8249,8250],{"class":89,"line":1059},[87,8251,142],{"emptyLinePlaceholder":141},[87,8253,8254,8257,8259,8261,8264],{"class":89,"line":3261},[87,8255,8256],{"class":148},"        data, err ",[87,8258,152],{"class":126},[87,8260,8165],{"class":148},[87,8262,8263],{"class":93},"ReadFile",[87,8265,8266],{"class":148},"(sub, p)\n",[87,8268,8269,8271,8273,8275,8277],{"class":89,"line":3269},[87,8270,1263],{"class":126},[87,8272,586],{"class":148},[87,8274,589],{"class":126},[87,8276,592],{"class":415},[87,8278,595],{"class":148},[87,8280,8281],{"class":89,"line":3277},[87,8282,8283],{"class":173},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[87,8285,8286,8289,8291,8293,8295,8298,8301],{"class":89,"line":3282},[87,8287,8288],{"class":148},"            data, _ ",[87,8290,1671],{"class":126},[87,8292,8165],{"class":148},[87,8294,8263],{"class":93},[87,8296,8297],{"class":148},"(sub, ",[87,8299,8300],{"class":97},"\"index.html\"",[87,8302,231],{"class":148},[87,8304,8305],{"class":89,"line":3288},[87,8306,1317],{"class":148},[87,8308,8309],{"class":89,"line":3297},[87,8310,142],{"emptyLinePlaceholder":141},[87,8312,8314,8316,8319,8322,8325,8328,8331],{"class":89,"line":8313},33,[87,8315,999],{"class":148},[87,8317,8318],{"class":93},"Data",[87,8320,8321],{"class":148},"(http.StatusOK, mime.",[87,8323,8324],{"class":93},"TypeByExtension",[87,8326,8327],{"class":148},"(filepath.",[87,8329,8330],{"class":93},"Ext",[87,8332,8333],{"class":148},"(p)), data)\n",[87,8335,8337],{"class":89,"line":8336},34,[87,8338,620],{"class":148},[87,8340,8342],{"class":89,"line":8341},35,[87,8343,1062],{"class":148},[73,8345,8347],{"id":8346},"_2-改造路由","2. 改造路由",[78,8349,8351],{"className":118,"code":8350,"language":94,"meta":83,"style":83},"\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",[30,8352,8353,8358,8369,8373,8378,8393,8407,8420,8433,8446],{"__ignoreMap":83},[87,8354,8355],{"class":89,"line":90},[87,8356,8357],{"class":173},"\u002F\u002F router.go\n",[87,8359,8360,8362,8364,8367],{"class":89,"line":104},[87,8361,127],{"class":126},[87,8363,130],{"class":97},[87,8365,8366],{"class":93},"myproject\u002Fweb",[87,8368,136],{"class":97},[87,8370,8371],{"class":89,"line":145},[87,8372,142],{"emptyLinePlaceholder":141},[87,8374,8375],{"class":89,"line":164},[87,8376,8377],{"class":173},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[87,8379,8380,8383,8385,8388,8391],{"class":89,"line":177},[87,8381,8382],{"class":148},"webHandler ",[87,8384,152],{"class":126},[87,8386,8387],{"class":148}," web.",[87,8389,8390],{"class":93},"SPAHandler",[87,8392,805],{"class":148},[87,8394,8395,8398,8400,8402,8404],{"class":89,"line":189},[87,8396,8397],{"class":148},"router.",[87,8399,967],{"class":93},[87,8401,219],{"class":148},[87,8403,8218],{"class":97},[87,8405,8406],{"class":148},", webHandler)\n",[87,8408,8409,8411,8413,8415,8418],{"class":89,"line":201},[87,8410,8397],{"class":148},[87,8412,967],{"class":93},[87,8414,219],{"class":148},[87,8416,8417],{"class":97},"\"\u002Fassets\u002F*filepath\"",[87,8419,8406],{"class":148},[87,8421,8422,8424,8426,8428,8431],{"class":89,"line":211},[87,8423,8397],{"class":148},[87,8425,967],{"class":93},[87,8427,219],{"class":148},[87,8429,8430],{"class":97},"\"\u002Ffavicon.ico\"",[87,8432,8406],{"class":148},[87,8434,8435,8437,8439,8441,8444],{"class":89,"line":228},[87,8436,8397],{"class":148},[87,8438,967],{"class":93},[87,8440,219],{"class":148},[87,8442,8443],{"class":97},"\"\u002Ffavicon.svg\"",[87,8445,8406],{"class":148},[87,8447,8448,8450,8453],{"class":89,"line":390},[87,8449,8397],{"class":148},[87,8451,8452],{"class":93},"NoRoute",[87,8454,8455],{"class":148},"(webHandler)\n",[73,8457,8459],{"id":8458},"_3-一键构建脚本","3. 一键构建脚本",[78,8461,8463],{"className":80,"code":8462,"language":82,"meta":83,"style":83},"#!\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",[30,8464,8465,8470,8475,8479,8487,8491,8496,8506,8516,8520,8525,8536,8549,8553,8558,8566,8579,8589],{"__ignoreMap":83},[87,8466,8467],{"class":89,"line":90},[87,8468,8469],{"class":173},"#!\u002Fbin\u002Fbash\n",[87,8471,8472],{"class":89,"line":104},[87,8473,8474],{"class":173},"# build-standalone.sh\n",[87,8476,8477],{"class":89,"line":145},[87,8478,142],{"emptyLinePlaceholder":141},[87,8480,8481,8484],{"class":89,"line":164},[87,8482,8483],{"class":415},"set",[87,8485,8486],{"class":415}," -e\n",[87,8488,8489],{"class":89,"line":177},[87,8490,142],{"emptyLinePlaceholder":141},[87,8492,8493],{"class":89,"line":189},[87,8494,8495],{"class":173},"# 1. 构建前端\n",[87,8497,8498,8501,8503],{"class":89,"line":201},[87,8499,8500],{"class":93},"pnpm",[87,8502,2251],{"class":97},[87,8504,8505],{"class":415}," --no-frozen-lockfile\n",[87,8507,8508,8510,8513],{"class":89,"line":211},[87,8509,3919],{"class":93},[87,8511,8512],{"class":97}," vite",[87,8514,8515],{"class":97}," build\n",[87,8517,8518],{"class":89,"line":228},[87,8519,142],{"emptyLinePlaceholder":141},[87,8521,8522],{"class":89,"line":390},[87,8523,8524],{"class":173},"# 2. 复制到嵌入目录\n",[87,8526,8527,8530,8533],{"class":89,"line":396},[87,8528,8529],{"class":93},"rm",[87,8531,8532],{"class":415}," -rf",[87,8534,8535],{"class":97}," service\u002Fweb\u002Fdist\n",[87,8537,8538,8541,8544,8547],{"class":89,"line":419},[87,8539,8540],{"class":93},"cp",[87,8542,8543],{"class":415}," -r",[87,8545,8546],{"class":97}," dist",[87,8548,8535],{"class":97},[87,8550,8551],{"class":89,"line":617},[87,8552,142],{"emptyLinePlaceholder":141},[87,8554,8555],{"class":89,"line":623},[87,8556,8557],{"class":173},"# 3. 编译 Go 二进制\n",[87,8559,8560,8563],{"class":89,"line":629},[87,8561,8562],{"class":415},"cd",[87,8564,8565],{"class":97}," service\n",[87,8567,8568,8570,8572,8574,8577],{"class":89,"line":647},[87,8569,94],{"class":93},[87,8571,2317],{"class":97},[87,8573,2326],{"class":415},[87,8575,8576],{"class":97}," myapp.exe",[87,8578,1092],{"class":415},[87,8580,8581,8584,8587],{"class":89,"line":927},[87,8582,8583],{"class":415},"    -ldflags=",[87,8585,8586],{"class":97},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[87,8588,1092],{"class":415},[87,8590,8591],{"class":89,"line":932},[87,8592,8593],{"class":97},"    main.go\n",[11,8595,8596],{"id":8596},"踩坑记录",[73,8598,8600],{"id":8599},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[16,8602,8603,8604,8606,8607,8609],{},"首次实现时用了 Gin 的 ",[30,8605,8452],{}," 兜底，但部分静态资源返回 404。排查发现 ",[30,8608,8452],{}," 在某些路由未显式注册时不会按预期触发。",[16,8611,8612,8615,8616,8619,8620,8623,8624,8626],{},[38,8613,8614],{},"解决","：显式注册 ",[30,8617,8618],{},"\u002Fassets\u002F*filepath","、",[30,8621,8622],{},"\u002Ffavicon.ico"," 等路由，",[30,8625,8452],{}," 仅作为兜底。",[73,8628,8630,8631,8633,8634,8637],{"id":8629},"坑二goembed-排除-_-开头文件","坑二：",[30,8632,7998],{}," 排除 ",[30,8635,8636],{},"_"," 开头文件",[16,8639,8640,8641,8644,8645,8647,8648,1178],{},"这是最坑的。Vite 构建产出了一个 ",[30,8642,8643],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[30,8646,7998],{}," 当模式为目录时，",[38,8649,8650,8651,8653,8654,8656],{},"硬编码排除了 ",[30,8652,8636],{}," 和 ",[30,8655,348],{}," 开头的文件",[78,8658,8660],{"className":118,"code":8659,"language":94,"meta":83,"style":83},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[30,8661,8662,8667],{"__ignoreMap":83},[87,8663,8664],{"class":89,"line":90},[87,8665,8666],{"class":173},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[87,8668,8669],{"class":89,"line":104},[87,8670,8119],{"class":173},[16,8672,8673,8674,1178],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[30,8675,8676],{},"Unexpected token '\u003C'",[16,8678,8679,8681,8682,8684],{},[38,8680,8614],{},"：改 Vite 配置，去除了 ",[30,8683,8636],{}," 前缀：",[78,8686,8690],{"className":8687,"code":8688,"language":8689,"meta":83,"style":83},"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",[30,8691,8692,8697,8704,8711,8718,8726,8764,8769,8774,8779],{"__ignoreMap":83},[87,8693,8694],{"class":89,"line":90},[87,8695,8696],{"class":173},"\u002F\u002F vite.config.ts\n",[87,8698,8699,8702],{"class":89,"line":104},[87,8700,8701],{"class":93},"build",[87,8703,1985],{"class":148},[87,8705,8706,8709],{"class":89,"line":145},[87,8707,8708],{"class":93},"  rollupOptions",[87,8710,1985],{"class":148},[87,8712,8713,8716],{"class":89,"line":164},[87,8714,8715],{"class":93},"    output",[87,8717,1985],{"class":148},[87,8719,8720,8723],{"class":89,"line":177},[87,8721,8722],{"class":93},"      chunkFileNames",[87,8724,8725],{"class":148},"(chunkInfo) {\n",[87,8727,8728,8730,8733,8736,8738,8741,8743,8745,8747,8749,8751,8753,8755,8757,8759,8761],{"class":89,"line":189},[87,8729,600],{"class":126},[87,8731,8732],{"class":97}," `assets\u002F${",[87,8734,8735],{"class":148},"chunkInfo",[87,8737,348],{"class":97},[87,8739,8740],{"class":148},"name",[87,8742,348],{"class":97},[87,8744,4638],{"class":93},[87,8746,219],{"class":97},[87,8748,4264],{"class":97},[87,8750,4251],{"class":126},[87,8752,8636],{"class":4254},[87,8754,4264],{"class":97},[87,8756,354],{"class":97},[87,8758,4655],{"class":97},[87,8760,4958],{"class":97},[87,8762,8763],{"class":97},"}-[hash].js`\n",[87,8765,8766],{"class":89,"line":201},[87,8767,8768],{"class":148},"      },\n",[87,8770,8771],{"class":89,"line":211},[87,8772,8773],{"class":148},"    },\n",[87,8775,8776],{"class":89,"line":228},[87,8777,8778],{"class":148},"  },\n",[87,8780,8781],{"class":89,"line":390},[87,8782,8783],{"class":148},"},\n",[16,8785,8786,8787,8789],{},"这样就避免产生 ",[30,8788,8636],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[73,8791,8793],{"id":8792},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[16,8795,8796,8797,8800],{},"多次 ",[30,8798,8799],{},"go build"," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[16,8802,8803,8805,8806,8809],{},[38,8804,8614],{},"：使用 ",[30,8807,8808],{},"go build -a"," 强制重新编译所有包。",[11,8811,8812],{"id":8812},"最终效果",[78,8814,8816],{"className":80,"code":8815,"language":82,"meta":83,"style":83},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[30,8817,8818,8828,8838],{"__ignoreMap":83},[87,8819,8820,8822,8825],{"class":89,"line":90},[87,8821,4261],{"class":93},[87,8823,8824],{"class":97}," bash",[87,8826,8827],{"class":97}," build-standalone.sh\n",[87,8829,8830,8832,8835],{"class":89,"line":104},[87,8831,4239],{"class":97},[87,8833,8834],{"class":97}," 构建成功",[87,8836,8837],{"class":97}," ===\n",[87,8839,8840,8843,8846],{"class":89,"line":145},[87,8841,8842],{"class":93},"输出:",[87,8844,8845],{"class":97}," service\u002Fmyapp.exe",[87,8847,8848],{"class":97},"  41M\n",[16,8850,8851],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[16,8853,8854],{},[38,8855,8856],{},"部署对比：",[2043,8858,8859,8872],{},[2046,8860,8861],{},[2049,8862,8863,8866,8869],{},[2052,8864,8865],{},"方式",[2052,8867,8868],{},"文件数",[2052,8870,8871],{},"操作",[2059,8873,8874,8885],{},[2049,8875,8876,8879,8882],{},[2064,8877,8878],{},"旧方案",[2064,8880,8881],{},"1 exe + 1 web 目录",[2064,8883,8884],{},"解压→保持目录结构",[2049,8886,8887,8890,8893],{},[2064,8888,8889],{},"新方案",[2064,8891,8892],{},"1 个 exe",[2064,8894,8895],{},"直接扔到服务器运行",[11,8897,2041],{"id":2041},[16,8899,8900,8901,8903],{},"Go 的 ",[30,8902,7998],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[48,8905,8906,8913],{},[51,8907,8908,8909,8653,8911,8656],{},"嵌入目录时自动排除 ",[30,8910,8636],{},[30,8912,348],{},[51,8914,8915],{},"构建缓存可能导致 embed 不是最新的",[16,8917,8918],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[2124,8920,8921],{},"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":83,"searchDepth":104,"depth":145,"links":8923},[8924,8925,8926,8931,8937,8938],{"id":4021,"depth":104,"text":4021},{"id":7992,"depth":104,"text":7992},{"id":8006,"depth":104,"text":8006,"children":8927},[8928,8929,8930],{"id":8009,"depth":145,"text":8010},{"id":8346,"depth":145,"text":8347},{"id":8458,"depth":145,"text":8459},{"id":8596,"depth":104,"text":8596,"children":8932},[8933,8934,8936],{"id":8599,"depth":145,"text":8600},{"id":8629,"depth":145,"text":8935},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":8792,"depth":145,"text":8793},{"id":8812,"depth":104,"text":8812},{"id":2041,"depth":104,"text":2041},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fgo-embed-frontend","2026-06-06T16:00:00",{"title":7975,"description":8939},"posts\u002Fgo-embed-frontend\u002Findex",[2158,8947,8948,8949],"Vue","编译","埋坑","2026-06-06","SIQNL24wVAFb18wIGWSB3lNVZUQ9B76jWgQ55TASTYk",{"id":8953,"title":8954,"author":6,"body":8955,"description":10351,"difficulty":3367,"draft":2148,"extension":2149,"image":10352,"meta":10353,"navigation":141,"path":10354,"pinned":2148,"published":10355,"seo":10356,"series":6,"seriesOrder":6,"stem":10357,"tags":10358,"updated":10355,"__hash__":10360},"posts\u002Fposts\u002Fjira-cli-intro\u002Findex.md","jira_cli — 把企业 Jira 装进命令行，AI Agent 直接调用",{"type":8,"value":8956,"toc":10330},[8957,8961,8976,9043,9046,9049,9126,9128,9161,9168,9171,9175,9287,9291,9422,9426,9494,9497,9560,9563,9565,9569,9572,9592,9595,9610,9613,9617,9623,9761,9772,9803,9807,9810,9907,9911,9914,10032,10036,10039,10103,10106,10110,10113,10181,10184,10190,10193,10227,10234,10237,10297,10299,10302,10319,10327],[11,8958,8960],{"id":8959},"jira_cli-是什么","jira_cli 是什么",[16,8962,8963,8968,8969,8653,8972,8975],{},[3974,8964,8967],{"href":8965,"rel":8966},"https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli",[3978],"jira_cli"," 是一个面向企业 Jira（Server \u002F Data Center）的本地命令行工具，Go 语言编写，MIT 协议开源。它同时提供 ",[38,8970,8971],{},"Cobra CLI",[38,8973,8974],{},"MCP stdio server"," 两种入口，既能当日常命令行用，又能直接接入 AI 编程工具。",[78,8977,8979],{"className":80,"code":8978,"language":82,"meta":83,"style":83},"# 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",[30,8980,8981,8986,8999,9010,9025,9029,9034],{"__ignoreMap":83},[87,8982,8983],{"class":89,"line":90},[87,8984,8985],{"class":173},"# CLI 日常使用\n",[87,8987,8988,8990,8993,8996],{"class":89,"line":104},[87,8989,8967],{"class":93},[87,8991,8992],{"class":97}," issue",[87,8994,8995],{"class":97}," search",[87,8997,8998],{"class":97}," \"project = PROJ ORDER BY updated DESC\"\n",[87,9000,9001,9003,9005,9007],{"class":89,"line":145},[87,9002,8967],{"class":93},[87,9004,8992],{"class":97},[87,9006,98],{"class":97},[87,9008,9009],{"class":97}," PROJ-123\n",[87,9011,9012,9014,9016,9019,9022],{"class":89,"line":164},[87,9013,8967],{"class":93},[87,9015,8992],{"class":97},[87,9017,9018],{"class":97}," comment",[87,9020,9021],{"class":97}," PROJ-123",[87,9023,9024],{"class":97}," \"处理完成，请测试验证\"\n",[87,9026,9027],{"class":89,"line":177},[87,9028,142],{"emptyLinePlaceholder":141},[87,9030,9031],{"class":89,"line":189},[87,9032,9033],{"class":173},"# MCP 模式 — 给 AI 装上操作 Jira 的手\n",[87,9035,9036,9038,9040],{"class":89,"line":201},[87,9037,8967],{"class":93},[87,9039,360],{"class":97},[87,9041,9042],{"class":415}," --user=work\n",[11,9044,9045],{"id":9045},"为什么需要它",[16,9047,9048],{},"大多数 Jira 命令行工具只解决\"开发者手动操作 Jira\"这一个场景。但 2026 年 AI 编程工具已经普及，你的 AI Agent（Claude Code、Codex、Cursor 等）也需要读写 Jira。痛点就来了：",[2043,9050,9051,9061],{},[2046,9052,9053],{},[2049,9054,9055,9058],{},[2052,9056,9057],{},"痛点",[2052,9059,9060],{},"jira_cli 怎么解",[2059,9062,9063,9071,9079,9091,9103,9114],{},[2049,9064,9065,9068],{},[2064,9066,9067],{},"企业 Jira 在内网，没有公网 API",[2064,9069,9070],{},"本地 stdio MCP server，不经过外网",[2049,9072,9073,9076],{},[2064,9074,9075],{},"AI 可能误删、误写 Jira 数据",[2064,9077,9078],{},"账号级只读策略，AI 只能查不能改",[2049,9080,9081,9084],{},[2064,9082,9083],{},"团队多人多 Jira 实例",[2064,9085,9086,9087,9090],{},"多 Profile 管理，",[30,9088,9089],{},"--user=work"," 切换",[2049,9092,9093,9096],{},[2064,9094,9095],{},"Windows 终端体验差",[2064,9097,9098,9099,9102],{},"原生 ",[30,9100,9101],{},".exe"," 构建 + 一键安装脚本",[2049,9104,9105,9108],{},[2064,9106,9107],{},"AI 生成 JQL 困难",[2064,9109,9110,9111],{},"字段元数据发现：",[30,9112,9113],{},"field list --query xxx",[2049,9115,9116,9119],{},[2064,9117,9118],{},"MCP 直接拿原始 JSON 太冗长",[2064,9120,9121,9122,9125],{},"紧凑视图 + ",[30,9123,9124],{},"--view summary"," 减少 token 消耗",[11,9127,7338],{"id":7338},[78,9129,9131],{"className":80,"code":9130,"language":82,"meta":83,"style":83},"# 从源码安装（要求 Go 1.26.3+）\ngo install github.com\u002Fyourname\u002Fjira_cli@latest\n\n# 或下载预编译二进制\n# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[30,9132,9133,9138,9147,9151,9156],{"__ignoreMap":83},[87,9134,9135],{"class":89,"line":90},[87,9136,9137],{"class":173},"# 从源码安装（要求 Go 1.26.3+）\n",[87,9139,9140,9142,9144],{"class":89,"line":104},[87,9141,94],{"class":93},[87,9143,2251],{"class":97},[87,9145,9146],{"class":97}," github.com\u002Fyourname\u002Fjira_cli@latest\n",[87,9148,9149],{"class":89,"line":145},[87,9150,142],{"emptyLinePlaceholder":141},[87,9152,9153],{"class":89,"line":164},[87,9154,9155],{"class":173},"# 或下载预编译二进制\n",[87,9157,9158],{"class":89,"line":177},[87,9159,9160],{"class":173},"# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[16,9162,9163,9164,9167],{},"Windows 用户下载 zip 包解压后双击 ",[30,9165,9166],{},"install.bat"," 即可全局安装，自动加入 PATH。",[11,9169,9170],{"id":9170},"快速上手",[73,9172,9174],{"id":9173},"_1-配置认证","1. 配置认证",[78,9176,9178],{"className":80,"code":9177,"language":82,"meta":83,"style":83},"# 设置 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",[30,9179,9180,9185,9201,9205,9210,9230,9234,9239,9268,9272,9277],{"__ignoreMap":83},[87,9181,9182],{"class":89,"line":90},[87,9183,9184],{"class":173},"# 设置 Jira 地址\n",[87,9186,9187,9189,9192,9195,9198],{"class":89,"line":104},[87,9188,8967],{"class":93},[87,9190,9191],{"class":97}," set",[87,9193,9194],{"class":97}," base-url",[87,9196,9197],{"class":97}," https:\u002F\u002Fjira.your-company.com",[87,9199,9200],{"class":415}," --global\n",[87,9202,9203],{"class":89,"line":145},[87,9204,142],{"emptyLinePlaceholder":141},[87,9206,9207],{"class":89,"line":164},[87,9208,9209],{"class":173},"# Token 认证（推荐）\n",[87,9211,9212,9214,9216,9219,9221,9224,9226,9228],{"class":89,"line":177},[87,9213,8967],{"class":93},[87,9215,9191],{"class":97},[87,9217,9218],{"class":97}," token",[87,9220,2693],{"class":126},[87,9222,9223],{"class":97},"your-toke",[87,9225,3095],{"class":148},[87,9227,2702],{"class":126},[87,9229,9200],{"class":415},[87,9231,9232],{"class":89,"line":189},[87,9233,142],{"emptyLinePlaceholder":141},[87,9235,9236],{"class":89,"line":201},[87,9237,9238],{"class":173},"# 或用户名密码认证\n",[87,9240,9241,9243,9245,9247,9249,9252,9254,9256,9258,9261,9264,9266],{"class":89,"line":211},[87,9242,8967],{"class":93},[87,9244,9191],{"class":97},[87,9246,7412],{"class":97},[87,9248,2693],{"class":126},[87,9250,9251],{"class":97},"usernam",[87,9253,3081],{"class":148},[87,9255,2702],{"class":126},[87,9257,2693],{"class":126},[87,9259,9260],{"class":97},"passwor",[87,9262,9263],{"class":148},"d",[87,9265,2702],{"class":126},[87,9267,9200],{"class":415},[87,9269,9270],{"class":89,"line":228},[87,9271,142],{"emptyLinePlaceholder":141},[87,9273,9274],{"class":89,"line":390},[87,9275,9276],{"class":173},"# 验证一下\n",[87,9278,9279,9281,9284],{"class":89,"line":396},[87,9280,8967],{"class":93},[87,9282,9283],{"class":97}," user",[87,9285,9286],{"class":97}," me\n",[73,9288,9290],{"id":9289},"_2-日常使用","2. 日常使用",[78,9292,9294],{"className":80,"code":9293,"language":82,"meta":83,"style":83},"# 查自己今天的任务\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",[30,9295,9296,9301,9312,9316,9321,9331,9335,9340,9354,9371,9375,9380,9393,9397,9402],{"__ignoreMap":83},[87,9297,9298],{"class":89,"line":90},[87,9299,9300],{"class":173},"# 查自己今天的任务\n",[87,9302,9303,9305,9307,9309],{"class":89,"line":104},[87,9304,8967],{"class":93},[87,9306,8992],{"class":97},[87,9308,8995],{"class":97},[87,9310,9311],{"class":97}," \"assignee = currentUser() ORDER BY updated DESC\"\n",[87,9313,9314],{"class":89,"line":145},[87,9315,142],{"emptyLinePlaceholder":141},[87,9317,9318],{"class":89,"line":164},[87,9319,9320],{"class":173},"# 看某个任务详情\n",[87,9322,9323,9325,9327,9329],{"class":89,"line":177},[87,9324,8967],{"class":93},[87,9326,8992],{"class":97},[87,9328,98],{"class":97},[87,9330,9009],{"class":97},[87,9332,9333],{"class":89,"line":189},[87,9334,142],{"emptyLinePlaceholder":141},[87,9336,9337],{"class":89,"line":201},[87,9338,9339],{"class":173},"# 流转状态\n",[87,9341,9342,9344,9346,9349,9351],{"class":89,"line":211},[87,9343,8967],{"class":93},[87,9345,8992],{"class":97},[87,9347,9348],{"class":97}," transitions",[87,9350,9021],{"class":97},[87,9352,9353],{"class":173},"                    # 可以转哪些\n",[87,9355,9356,9358,9360,9363,9365,9368],{"class":89,"line":228},[87,9357,8967],{"class":93},[87,9359,8992],{"class":97},[87,9361,9362],{"class":97}," do-transition",[87,9364,9021],{"class":97},[87,9366,9367],{"class":415}," 21",[87,9369,9370],{"class":173},"               # 执行流转\n",[87,9372,9373],{"class":89,"line":390},[87,9374,142],{"emptyLinePlaceholder":141},[87,9376,9377],{"class":89,"line":396},[87,9378,9379],{"class":173},"# 添加备注\n",[87,9381,9382,9384,9386,9388,9390],{"class":89,"line":419},[87,9383,8967],{"class":93},[87,9385,8992],{"class":97},[87,9387,9018],{"class":97},[87,9389,9021],{"class":97},[87,9391,9392],{"class":97}," \"已定位到问题，正在修复\"\n",[87,9394,9395],{"class":89,"line":617},[87,9396,142],{"emptyLinePlaceholder":141},[87,9398,9399],{"class":89,"line":623},[87,9400,9401],{"class":173},"# 创建任务\n",[87,9403,9404,9406,9408,9410,9413,9416,9419],{"class":89,"line":629},[87,9405,8967],{"class":93},[87,9407,8992],{"class":97},[87,9409,7298],{"class":97},[87,9411,9412],{"class":97}," PROJ",[87,9414,9415],{"class":97}," Task",[87,9417,9418],{"class":415}," --fields",[87,9420,9421],{"class":97}," @fields.json\n",[73,9423,9425],{"id":9424},"_3-配给-ai-使用","3. 配给 AI 使用",[78,9427,9429],{"className":80,"code":9428,"language":82,"meta":83,"style":83},"# 先配置一个只读账号给 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",[30,9430,9431,9436,9453,9476],{"__ignoreMap":83},[87,9432,9433],{"class":89,"line":90},[87,9434,9435],{"class":173},"# 先配置一个只读账号给 AI 用\n",[87,9437,9438,9440,9442,9444,9446,9448,9451],{"class":89,"line":104},[87,9439,8967],{"class":93},[87,9441,9191],{"class":97},[87,9443,9194],{"class":97},[87,9445,9197],{"class":97},[87,9447,2690],{"class":415},[87,9449,9450],{"class":97}," ai",[87,9452,9200],{"class":415},[87,9454,9455,9457,9459,9461,9463,9466,9468,9470,9472,9474],{"class":89,"line":145},[87,9456,8967],{"class":93},[87,9458,9191],{"class":97},[87,9460,9218],{"class":97},[87,9462,2693],{"class":126},[87,9464,9465],{"class":97},"toke",[87,9467,3095],{"class":148},[87,9469,2702],{"class":126},[87,9471,2690],{"class":415},[87,9473,9450],{"class":97},[87,9475,9200],{"class":415},[87,9477,9478,9480,9482,9485,9488,9490,9492],{"class":89,"line":164},[87,9479,8967],{"class":93},[87,9481,9191],{"class":97},[87,9483,9484],{"class":97}," readonly",[87,9486,9487],{"class":97}," on",[87,9489,2690],{"class":415},[87,9491,9450],{"class":97},[87,9493,9200],{"class":415},[16,9495,9496],{},"然后在 AI 工具的 MCP 配置里加上：",[78,9498,9500],{"className":1968,"code":9499,"language":1970,"meta":83,"style":83},"{\n  \"mcpServers\": {\n    \"jira\": {\n      \"command\": \"jira_cli\",\n      \"args\": [\"mcp\", \"--user=ai\"]\n    }\n  }\n}\n",[30,9501,9502,9506,9512,9519,9531,9548,9552,9556],{"__ignoreMap":83},[87,9503,9504],{"class":89,"line":90},[87,9505,1977],{"class":148},[87,9507,9508,9510],{"class":89,"line":104},[87,9509,1982],{"class":415},[87,9511,1985],{"class":148},[87,9513,9514,9517],{"class":89,"line":145},[87,9515,9516],{"class":415},"    \"jira\"",[87,9518,1985],{"class":148},[87,9520,9521,9524,9526,9529],{"class":89,"line":164},[87,9522,9523],{"class":415},"      \"command\"",[87,9525,1025],{"class":148},[87,9527,9528],{"class":97},"\"jira_cli\"",[87,9530,294],{"class":148},[87,9532,9533,9536,9538,9541,9543,9546],{"class":89,"line":177},[87,9534,9535],{"class":415},"      \"args\"",[87,9537,3889],{"class":148},[87,9539,9540],{"class":97},"\"mcp\"",[87,9542,354],{"class":148},[87,9544,9545],{"class":97},"\"--user=ai\"",[87,9547,2615],{"class":148},[87,9549,9550],{"class":89,"line":189},[87,9551,620],{"class":148},[87,9553,9554],{"class":89,"line":201},[87,9555,2034],{"class":148},[87,9557,9558],{"class":89,"line":211},[87,9559,1062],{"class":148},[16,9561,9562],{},"这样 AI 就能查 Jira 但不能修改，既有了上下文，又不用担心误操作。",[11,9564,7820],{"id":7820},[73,9566,9568],{"id":9567},"_1-账号级只读策略","1. 账号级只读策略",[16,9570,9571],{},"这是 jira_cli 最实用的设计。给 AI Agent 配置只读账号：",[78,9573,9575],{"className":80,"code":9574,"language":82,"meta":83,"style":83},"jira_cli set readonly on --name ai\n",[30,9576,9577],{"__ignoreMap":83},[87,9578,9579,9581,9583,9585,9587,9589],{"class":89,"line":90},[87,9580,8967],{"class":93},[87,9582,9191],{"class":97},[87,9584,9484],{"class":97},[87,9586,9487],{"class":97},[87,9588,2690],{"class":415},[87,9590,9591],{"class":97}," ai\n",[16,9593,9594],{},"只读模式下：",[3705,9596,9597,9600,9603],{},[51,9598,9599],{},"CLI 写命令被隐藏和拒绝",[51,9601,9602],{},"MCP 的写入工具不会注册",[51,9604,9605,9606,9609],{},"环境变量 ",[30,9607,9608],{},"JIRA_READONLY=true"," 也可以临时覆盖",[16,9611,9612],{},"AI 只能查 JQL、看任务、查字段、读评论，绝对不会误改数据。需要创建\u002F更新时，另配一个有写入权限的账号由开发者自己操作。",[73,9614,9616],{"id":9615},"_2-多-profile-切换","2. 多 Profile 切换",[16,9618,9619,9620,9622],{},"一个配置文件里管理多个 Jira 账号，",[30,9621,3046],{}," 切换：",[78,9624,9626],{"className":80,"code":9625,"language":82,"meta":83,"style":83},"# 工作账号\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",[30,9627,9628,9633,9651,9674,9678,9683,9701,9724,9728,9733,9747],{"__ignoreMap":83},[87,9629,9630],{"class":89,"line":90},[87,9631,9632],{"class":173},"# 工作账号\n",[87,9634,9635,9637,9639,9641,9644,9646,9649],{"class":89,"line":104},[87,9636,8967],{"class":93},[87,9638,9191],{"class":97},[87,9640,9194],{"class":97},[87,9642,9643],{"class":97}," https:\u002F\u002Fjira.company.com",[87,9645,2690],{"class":415},[87,9647,9648],{"class":97}," work",[87,9650,9200],{"class":415},[87,9652,9653,9655,9657,9659,9661,9664,9666,9668,9670,9672],{"class":89,"line":145},[87,9654,8967],{"class":93},[87,9656,9191],{"class":97},[87,9658,9218],{"class":97},[87,9660,2693],{"class":126},[87,9662,9663],{"class":97},"work-toke",[87,9665,3095],{"class":148},[87,9667,2702],{"class":126},[87,9669,2690],{"class":415},[87,9671,9648],{"class":97},[87,9673,9200],{"class":415},[87,9675,9676],{"class":89,"line":164},[87,9677,142],{"emptyLinePlaceholder":141},[87,9679,9680],{"class":89,"line":177},[87,9681,9682],{"class":173},"# 个人账号\n",[87,9684,9685,9687,9689,9691,9694,9696,9699],{"class":89,"line":189},[87,9686,8967],{"class":93},[87,9688,9191],{"class":97},[87,9690,9194],{"class":97},[87,9692,9693],{"class":97}," https:\u002F\u002Fjira.personal.com",[87,9695,2690],{"class":415},[87,9697,9698],{"class":97}," personal",[87,9700,9200],{"class":415},[87,9702,9703,9705,9707,9709,9711,9714,9716,9718,9720,9722],{"class":89,"line":201},[87,9704,8967],{"class":93},[87,9706,9191],{"class":97},[87,9708,9218],{"class":97},[87,9710,2693],{"class":126},[87,9712,9713],{"class":97},"personal-toke",[87,9715,3095],{"class":148},[87,9717,2702],{"class":126},[87,9719,2690],{"class":415},[87,9721,9698],{"class":97},[87,9723,9200],{"class":415},[87,9725,9726],{"class":89,"line":211},[87,9727,142],{"emptyLinePlaceholder":141},[87,9729,9730],{"class":89,"line":228},[87,9731,9732],{"class":173},"# 使用时切换\n",[87,9734,9735,9737,9740,9742,9744],{"class":89,"line":390},[87,9736,8967],{"class":93},[87,9738,9739],{"class":415}," --user=work",[87,9741,8992],{"class":97},[87,9743,8995],{"class":97},[87,9745,9746],{"class":97}," \"project = PROJ\"\n",[87,9748,9749,9751,9754,9756,9758],{"class":89,"line":396},[87,9750,8967],{"class":93},[87,9752,9753],{"class":415}," --user=personal",[87,9755,8992],{"class":97},[87,9757,8995],{"class":97},[87,9759,9760],{"class":97}," \"project = SIDE\"\n",[16,9762,9763,9764,9767,9768,9771],{},"同时支持",[38,9765,9766],{},"项目级配置","（",[30,9769,9770],{},"--local","）和工作目录绑定：",[78,9773,9775],{"className":80,"code":9774,"language":82,"meta":83,"style":83},"# 在项目目录下，配置只对当前项目生效\ncd ~\u002Fprojects\u002Fbackend\njira_cli set base-url https:\u002F\u002Fjira-backend.company.com --local\n",[30,9776,9777,9782,9789],{"__ignoreMap":83},[87,9778,9779],{"class":89,"line":90},[87,9780,9781],{"class":173},"# 在项目目录下，配置只对当前项目生效\n",[87,9783,9784,9786],{"class":89,"line":104},[87,9785,8562],{"class":415},[87,9787,9788],{"class":97}," ~\u002Fprojects\u002Fbackend\n",[87,9790,9791,9793,9795,9797,9800],{"class":89,"line":145},[87,9792,8967],{"class":93},[87,9794,9191],{"class":97},[87,9796,9194],{"class":97},[87,9798,9799],{"class":97}," https:\u002F\u002Fjira-backend.company.com",[87,9801,9802],{"class":415}," --local\n",[73,9804,9806],{"id":9805},"_3-对-ai-友好的输出","3. 对 AI 友好的输出",[16,9808,9809],{},"jira_cli 在输出设计上充分考虑了 AI Agent 的 token 消耗：",[78,9811,9813],{"className":80,"code":9812,"language":82,"meta":83,"style":83},"# 紧凑视图 — 去掉 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",[30,9814,9815,9820,9836,9840,9845,9862,9866,9871,9886,9890,9895],{"__ignoreMap":83},[87,9816,9817],{"class":89,"line":90},[87,9818,9819],{"class":173},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\n",[87,9821,9822,9824,9826,9828,9830,9833],{"class":89,"line":104},[87,9823,8967],{"class":93},[87,9825,8992],{"class":97},[87,9827,98],{"class":97},[87,9829,9021],{"class":97},[87,9831,9832],{"class":415}," --view",[87,9834,9835],{"class":97}," summary\n",[87,9837,9838],{"class":89,"line":145},[87,9839,142],{"emptyLinePlaceholder":141},[87,9841,9842],{"class":89,"line":164},[87,9843,9844],{"class":173},"# 表格输出 — 一眼看到关键信息\n",[87,9846,9847,9849,9851,9853,9856,9859],{"class":89,"line":177},[87,9848,8967],{"class":93},[87,9850,8992],{"class":97},[87,9852,8995],{"class":97},[87,9854,9855],{"class":97}," \"project = PROJ\"",[87,9857,9858],{"class":415}," --output",[87,9860,9861],{"class":97}," table\n",[87,9863,9864],{"class":89,"line":189},[87,9865,142],{"emptyLinePlaceholder":141},[87,9867,9868],{"class":89,"line":201},[87,9869,9870],{"class":173},"# JSON 输出 — 程序解析，不转义 Unicode\n",[87,9872,9873,9875,9877,9880,9882,9884],{"class":89,"line":211},[87,9874,8967],{"class":93},[87,9876,9858],{"class":415},[87,9878,9879],{"class":97}," json",[87,9881,8992],{"class":97},[87,9883,98],{"class":97},[87,9885,9009],{"class":97},[87,9887,9888],{"class":89,"line":228},[87,9889,142],{"emptyLinePlaceholder":141},[87,9891,9892],{"class":89,"line":390},[87,9893,9894],{"class":173},"# 自定义搜索列 — 只要需要的字段\n",[87,9896,9897,9899,9901,9904],{"class":89,"line":396},[87,9898,8967],{"class":93},[87,9900,9191],{"class":97},[87,9902,9903],{"class":97}," search-columns",[87,9905,9906],{"class":97}," key,summary,fixVersions\n",[73,9908,9910],{"id":9909},"_4-字段元数据发现","4. 字段元数据发现",[16,9912,9913],{},"Jira 的 customfield 让人头疼，jira_cli 提供了完整的字段发现能力：",[78,9915,9917],{"className":80,"code":9916,"language":82,"meta":83,"style":83},"# 列出所有字段及其类型\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",[30,9918,9919,9924,9933,9937,9942,9956,9960,9965,9976,9980,9985,10002,10006,10011],{"__ignoreMap":83},[87,9920,9921],{"class":89,"line":90},[87,9922,9923],{"class":173},"# 列出所有字段及其类型\n",[87,9925,9926,9928,9931],{"class":89,"line":104},[87,9927,8967],{"class":93},[87,9929,9930],{"class":97}," field",[87,9932,4201],{"class":97},[87,9934,9935],{"class":89,"line":145},[87,9936,142],{"emptyLinePlaceholder":141},[87,9938,9939],{"class":89,"line":164},[87,9940,9941],{"class":173},"# 搜索包含特定关键词的字段\n",[87,9943,9944,9946,9948,9950,9953],{"class":89,"line":177},[87,9945,8967],{"class":93},[87,9947,9930],{"class":97},[87,9949,7233],{"class":97},[87,9951,9952],{"class":415}," --query",[87,9954,9955],{"class":97}," \"版本\"\n",[87,9957,9958],{"class":89,"line":189},[87,9959,142],{"emptyLinePlaceholder":141},[87,9961,9962],{"class":89,"line":201},[87,9963,9964],{"class":173},"# 只看自定义字段\n",[87,9966,9967,9969,9971,9973],{"class":89,"line":211},[87,9968,8967],{"class":93},[87,9970,9930],{"class":97},[87,9972,7233],{"class":97},[87,9974,9975],{"class":415}," --custom-only\n",[87,9977,9978],{"class":89,"line":228},[87,9979,142],{"emptyLinePlaceholder":141},[87,9981,9982],{"class":89,"line":390},[87,9983,9984],{"class":173},"# 创建任务前，先看看需要哪些字段\n",[87,9986,9987,9989,9991,9994,9996,9999],{"class":89,"line":396},[87,9988,8967],{"class":93},[87,9990,8992],{"class":97},[87,9992,9993],{"class":97}," create-meta",[87,9995,9412],{"class":97},[87,9997,9998],{"class":415}," --issue-type",[87,10000,10001],{"class":97}," Task\n",[87,10003,10004],{"class":89,"line":419},[87,10005,142],{"emptyLinePlaceholder":141},[87,10007,10008],{"class":89,"line":617},[87,10009,10010],{"class":173},"# 分页查看可选值\n",[87,10012,10013,10015,10017,10020,10022,10024,10027,10029],{"class":89,"line":623},[87,10014,8967],{"class":93},[87,10016,8992],{"class":97},[87,10018,10019],{"class":97}," create-meta-values",[87,10021,9412],{"class":97},[87,10023,9415],{"class":97},[87,10025,10026],{"class":97}," priority",[87,10028,9952],{"class":415},[87,10030,10031],{"class":97}," High\n",[73,10033,10035],{"id":10034},"_5-mcp-写入控制","5. MCP 写入控制",[16,10037,10038],{},"除了全局只读，MCP 还有更细粒度的控制：",[2043,10040,10041,10051],{},[2046,10042,10043],{},[2049,10044,10045,10048],{},[2052,10046,10047],{},"环境变量",[2052,10049,10050],{},"作用",[2059,10052,10053,10063,10073,10083,10093],{},[2049,10054,10055,10060],{},[2064,10056,10057],{},[30,10058,10059],{},"JIRA_CREATE_OPEN=false",[2064,10061,10062],{},"禁用创建\u002F更新工具",[2049,10064,10065,10070],{},[2064,10066,10067],{},[30,10068,10069],{},"JIRA_DISABLE_DISCLAIMER=true",[2064,10071,10072],{},"去掉 AI 自动添加的备注声明",[2049,10074,10075,10080],{},[2064,10076,10077],{},[30,10078,10079],{},"JIRA_CREATE_SUMMARY_MAX",[2064,10081,10082],{},"限制摘要长度",[2049,10084,10085,10090],{},[2064,10086,10087],{},[30,10088,10089],{},"JIRA_CREATE_DESCRIPTION_MAX",[2064,10091,10092],{},"限制描述长度",[2049,10094,10095,10100],{},[2064,10096,10097],{},[30,10098,10099],{},"JIRA_CREATE_FIELDS_MAX_BYTES",[2064,10101,10102],{},"限制创建字段的 JSON 大小",[16,10104,10105],{},"这些限制确保 AI 即使有写入权限也不会\"失控\"——例如防止 AI 往描述里塞几万字的废话。",[73,10107,10109],{"id":10108},"_6-中英双语帮助","6. 中英双语帮助",[16,10111,10112],{},"帮助信息默认中文，也支持一键切英文：",[78,10114,10116],{"className":80,"code":10115,"language":82,"meta":83,"style":83},"# 保存英文偏好\njira_cli set help-language en\n\n# 切回中文\njira_cli set help-language zh\n\n# 临时覆盖\njira_cli --lang en issue search -h\n",[30,10117,10118,10123,10135,10139,10144,10155,10159,10164],{"__ignoreMap":83},[87,10119,10120],{"class":89,"line":90},[87,10121,10122],{"class":173},"# 保存英文偏好\n",[87,10124,10125,10127,10129,10132],{"class":89,"line":104},[87,10126,8967],{"class":93},[87,10128,9191],{"class":97},[87,10130,10131],{"class":97}," help-language",[87,10133,10134],{"class":97}," en\n",[87,10136,10137],{"class":89,"line":145},[87,10138,142],{"emptyLinePlaceholder":141},[87,10140,10141],{"class":89,"line":164},[87,10142,10143],{"class":173},"# 切回中文\n",[87,10145,10146,10148,10150,10152],{"class":89,"line":177},[87,10147,8967],{"class":93},[87,10149,9191],{"class":97},[87,10151,10131],{"class":97},[87,10153,10154],{"class":97}," zh\n",[87,10156,10157],{"class":89,"line":189},[87,10158,142],{"emptyLinePlaceholder":141},[87,10160,10161],{"class":89,"line":201},[87,10162,10163],{"class":173},"# 临时覆盖\n",[87,10165,10166,10168,10171,10174,10176,10178],{"class":89,"line":211},[87,10167,8967],{"class":93},[87,10169,10170],{"class":415}," --lang",[87,10172,10173],{"class":97}," en",[87,10175,8992],{"class":97},[87,10177,8995],{"class":97},[87,10179,10180],{"class":415}," -h\n",[11,10182,10183],{"id":10183},"架构一览",[78,10185,10188],{"className":10186,"code":10187,"language":3474},[3472],"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",[30,10189,10187],{"__ignoreMap":83},[16,10191,10192],{},"设计原则清晰：",[3705,10194,10195,10203,10211,10219],{},[51,10196,10197,10202],{},[38,10198,10199],{},[30,10200,10201],{},"pkg\u002Fjira"," 是纯 Jira 客户端，HTTP、认证、上下文处理都在这里",[51,10204,10205,10210],{},[38,10206,10207],{},[30,10208,10209],{},"cmd"," 只管 Cobra 解析和呈现",[51,10212,10213,10218],{},[38,10214,10215],{},[30,10216,10217],{},"internal\u002Ftools\u002Fjira_tools"," 只管 MCP schema 验证和结果适配",[51,10220,10221,10226],{},[38,10222,10223],{},[30,10224,10225],{},"internal\u002Fconfig"," 拥有 Profile 的全部逻辑，不分散到 cmd",[16,10228,10229,10230,10233],{},"每一层职责单一，新增 Jira 能力时按 ",[30,10231,10232],{},"endpoints.go → Client 方法 → CLI 命令 → MCP tool → 策略注册"," 的路径走就行。",[11,10235,10236],{"id":10236},"和已有方案的对比",[2043,10238,10239,10251],{},[2046,10240,10241],{},[2049,10242,10243,10245,10248],{},[2052,10244,4816],{},[2052,10246,10247],{},"适合场景",[2052,10249,10250],{},"jira_cli 的区别",[2059,10252,10253,10264,10275,10286],{},[2049,10254,10255,10258,10261],{},[2064,10256,10257],{},"Atlassian Rovo MCP Server",[2064,10259,10260],{},"Jira Cloud、官方 OAuth",[2064,10262,10263],{},"jira_cli 是本地 stdio，适合内网 Server\u002FDC",[2049,10265,10266,10269,10272],{},[2064,10267,10268],{},"go-jira \u002F jira-cli 社区版",[2064,10270,10271],{},"纯命令行管理",[2064,10273,10274],{},"jira_cli 同时提供 MCP server，面向 AI 工作流",[2049,10276,10277,10280,10283],{},[2064,10278,10279],{},"直接调 REST API",[2064,10281,10282],{},"自定义集成",[2064,10284,10285],{},"jira_cli 封装了认证、只读策略、字段元数据",[2049,10287,10288,10291,10294],{},[2064,10289,10290],{},"Jira 网页",[2064,10292,10293],{},"日常使用",[2064,10295,10296],{},"jira_cli 把 Jira 变成终端里的一行命令",[11,10298,2041],{"id":2041},[16,10300,10301],{},"如果你的团队满足以下任一条件，jira_cli 值得一试：",[3705,10303,10304,10307,10310,10313,10316],{},[51,10305,10306],{},"企业 Jira 部署在内网，想接入 AI 编程工具",[51,10308,10309],{},"想给 AI Agent 只读权限来查 Jira 上下文",[51,10311,10312],{},"需要命令行快速查看\u002F操作 Jira，不想切浏览器",[51,10314,10315],{},"需要管理多个 Jira 实例和多套账号",[51,10317,10318],{},"Windows 用户想要无摩擦的 CLI 体验",[16,10320,10321,10322,10326],{},"GitHub：",[3974,10323,10325],{"href":8965,"rel":10324},[3978],"github.com\u002Fyourname\u002Fjira_cli"," | MIT 协议 | Go 1.26.3+",[2124,10328,10329],{},"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":83,"searchDepth":104,"depth":145,"links":10331},[10332,10333,10334,10335,10340,10348,10349,10350],{"id":8959,"depth":104,"text":8960},{"id":9045,"depth":104,"text":9045},{"id":7338,"depth":104,"text":7338},{"id":9170,"depth":104,"text":9170,"children":10336},[10337,10338,10339],{"id":9173,"depth":145,"text":9174},{"id":9289,"depth":145,"text":9290},{"id":9424,"depth":145,"text":9425},{"id":7820,"depth":104,"text":7820,"children":10341},[10342,10343,10344,10345,10346,10347],{"id":9567,"depth":145,"text":9568},{"id":9615,"depth":145,"text":9616},{"id":9805,"depth":145,"text":9806},{"id":9909,"depth":145,"text":9910},{"id":10034,"depth":145,"text":10035},{"id":10108,"depth":145,"text":10109},{"id":10183,"depth":104,"text":10183},{"id":10236,"depth":104,"text":10236},{"id":2041,"depth":104,"text":2041},"介绍 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":8954,"description":10351},"posts\u002Fjira-cli-intro\u002Findex",[10359,7970,2159,2158,2161,7971],"Jira","vNH6sF2mMFmSjbHoEI2OqmYCgpFmaqyKt-MUfcQqXrU",{"id":10362,"title":10363,"author":6,"body":10364,"description":10908,"difficulty":3367,"draft":2148,"extension":2149,"image":10909,"meta":10910,"navigation":141,"path":10911,"pinned":2148,"published":10912,"seo":10913,"series":10914,"seriesOrder":90,"stem":10915,"tags":10916,"updated":10919,"__hash__":10920},"posts\u002Fposts\u002Fsublog-evolution\u002Findex.md","尼克的小窝 架构变迁：从纯静态到前后端分离",{"type":8,"value":10365,"toc":10888},[10366,10370,10373,10384,10388,10392,10398,10402,10433,10436,10450,10454,10457,10525,10529,10532,10538,10542,10552,10582,10585,10589,10597,10600,10644,10648,10769,10773,10780,10784,10790,10796,10800,10803,10854,10858,10865,10872,10878,10881,10883],[11,10367,10369],{"id":10368},"写在前面","📝 写在前面",[16,10371,10372],{},"SuBlog 最初只是一个简单的技术博客——Nuxt 3 静态生成，Markdown 写文章，部署到静态托管完事。但随着功能需求的增长，纯静态的架构开始显露出局限性。",[16,10374,10375,10376,10379,10380,10383],{},"这篇文章记录从 ",[38,10377,10378],{},"纯静态"," 到 ",[38,10381,10382],{},"前后端分离"," 的演进过程，带你了解每一步背后的思考。",[11,10385,10387],{"id":10386},"第一代纯静态nuxt-3-nuxtcontent","📦 第一代：纯静态（Nuxt 3 + @nuxt\u002Fcontent）",[73,10389,10391],{"id":10390},"️-架构图","🏗️ 架构图",[78,10393,10396],{"className":10394,"code":10395,"language":3474,"meta":83},[3472],"┌─────────────────────────────────────┐\n│          静态托管 (Nginx\u002FVercel)      │\n│                                     │\n│  ┌───────────────────────────────┐  │\n│  │     Nuxt 3 SSG 产物            │  │\n│  │                               │  │\n│  │  pages\u002F + content\u002F + public\u002F  │  │\n│  └───────────────────────────────┘  │\n│                                     │\n│  Giscus (GitHub Discussions 评论)    │\n└─────────────────────────────────────┘\n",[30,10397,10395],{"__ignoreMap":83},[73,10399,10401],{"id":10400},"做了什么","🔧 做了什么",[3705,10403,10404,10413,10418,10423],{},[51,10405,10406,2077,10408,2077,10410,10412],{},[38,10407,3644],{},[38,10409,3654],{},[38,10411,3664],{}," 搭建前端",[51,10414,10415,10417],{},[38,10416,3674],{}," 管理 Markdown 文章，编译时生成静态页面",[51,10419,10420,10422],{},[38,10421,3694],{}," 嵌入评论，依赖 GitHub Discussions",[51,10424,10425,10426,10429,10430,10432],{},"部署通过 ",[30,10427,10428],{},"nuxt generate"," 输出 ",[30,10431,3931],{},"，丢到 Nginx 或 Vercel",[73,10434,10435],{"id":4819},"✅ 优点",[3705,10437,10438,10444,10447],{},[51,10439,10440,10441,10443],{},"部署极简，一个 ",[30,10442,10428],{}," 搞定",[51,10445,10446],{},"加载飞快，全是 HTML，没有服务端开销",[51,10448,10449],{},"无需后端，零运维成本",[73,10451,10453],{"id":10452},"局限","❌ 局限",[16,10455,10456],{},"随着时间的推移，纯静态的问题越来越明显：",[2043,10458,10459,10468],{},[2046,10460,10461],{},[2049,10462,10463,10465],{},[2052,10464,1156],{},[2052,10466,10467],{},"具体表现",[2059,10469,10470,10481,10492,10503,10514],{},[2049,10471,10472,10478],{},[2064,10473,10474,10475],{},"🐢 ",[38,10476,10477],{},"内容更新慢",[2064,10479,10480],{},"改一篇文章要重新构建、重新部署，不能实时发布",[2049,10482,10483,10489],{},[2064,10484,10485,10486],{},"👤 ",[38,10487,10488],{},"没有用户体系",[2064,10490,10491],{},"无法区分读者和管理员，所有内容对所有人一致可见",[2049,10493,10494,10500],{},[2064,10495,10496,10497],{},"🔒 ",[38,10498,10499],{},"评论受限",[2064,10501,10502],{},"Giscus 需要 GitHub 账号才能评论，门槛高",[2049,10504,10505,10511],{},[2064,10506,10507,10508],{},"🚫 ",[38,10509,10510],{},"无法做动态功能",[2064,10512,10513],{},"点赞、收藏、阅读统计、个性化推荐……统统做不了",[2049,10515,10516,10522],{},[2064,10517,10518,10519],{},"🔗 ",[38,10520,10521],{},"前端锁死",[2064,10523,10524],{},"被 Nuxt\u002FVue 生态绑定，想换技术栈得重写",[11,10526,10528],{"id":10527},"第二代前后端分离react-go","⚡ 第二代：前后端分离（React + Go）",[73,10530,10391],{"id":10531},"️-架构图-1",[78,10533,10536],{"className":10534,"code":10535,"language":3474,"meta":83},[3472],"┌───────────────────────┐       ┌────────────────────┐\n│    静态托管 (Vercel)    │       │   Go 服务端         │\n│                       │       │                    │\n│  ┌─────────────────┐  │       │  ┌──────────────┐  │\n│  │   sublog-react   │  │  API  │  │  HTTP API    │  │\n│  │  (Next.js SSG)  │──┼───────┼─>│              │  │\n│  │                  │  │       │  │ 文章 \u002F 用户  │  │\n│  │  首页 \u002F 文章 \u002F   │  │       │  └──────────────┘  │\n│  │  用户中心        │  │       │                    │\n│  └─────────────────┘  │       │  ┌──────────────┐  │\n│                       │       │  │   数据库      │  │\n└───────────────────────┘       │  └──────────────┘  │\n                                └────────────────────┘\n",[30,10537,10535],{"__ignoreMap":83},[73,10539,10541],{"id":10540},"️-前端sublog-react","🖥️ 前端：sublog-react",[16,10543,10544,10545,10547,10548,10551],{},"选用 ",[38,10546,3501],{},"（App Router） + ",[38,10549,10550],{},"React 19","，理由是：",[3705,10553,10554,10561,10568,10575],{},[51,10555,10556,10557,10560],{},"🎨 ",[38,10558,10559],{},"React 生态","——组件库成熟，开发效率高",[51,10562,10563,10564,10567],{},"📐 ",[38,10565,10566],{},"App Router","——支持布局嵌套，页面组织清晰",[51,10569,10570,10571,10574],{},"📦 ",[38,10572,10573],{},"静态导出","——仍可部署到静态托管",[51,10576,10577,10578,10581],{},"⚙️ ",[38,10579,10580],{},"运行时配置","——API 地址可动态注入，切换环境无需重新构建",[16,10583,10584],{},"前端不再直接读取 Markdown 文件，而是通过 HTTP API 获取文章数据。静态导出 + 客户端数据获取的组合，既保留了首屏加载速度，又实现了内容的动态更新。",[73,10586,10588],{"id":10587},"️-后端sublog-s","⚙️ 后端：sublog-s",[16,10590,10591,10592,2077,10594,10596],{},"后端使用 ",[38,10593,2158],{},[38,10595,2160],{},"，按业务逻辑分层组织代码。",[16,10598,10599],{},"核心技术选型：",[2043,10601,10602,10610],{},[2046,10603,10604],{},[2049,10605,10606,10608],{},[2052,10607,3632],{},[2052,10609,3635],{},[2059,10611,10612,10622,10633],{},[2049,10613,10614,10619],{},[2064,10615,10616,10617],{},"🚀 ",[38,10618,2160],{},[2064,10620,10621],{},"HTTP 框架",[2049,10623,10624,10630],{},[2064,10625,10626,10627],{},"🗄️ ",[38,10628,10629],{},"GORM",[2064,10631,10632],{},"ORM，数据库操作",[2049,10634,10635,10641],{},[2064,10636,10637,10638],{},"🔐 ",[38,10639,10640],{},"JWT",[2064,10642,10643],{},"用户认证",[11,10645,10647],{"id":10646},"前后对比","📊 前后对比",[2043,10649,10650,10663],{},[2046,10651,10652],{},[2049,10653,10654,10657,10660],{},[2052,10655,10656],{},"维度",[2052,10658,10659],{},"第一代（纯静态）",[2052,10661,10662],{},"第二代（前后端分离）",[2059,10664,10665,10677,10689,10703,10716,10729,10743,10755],{},[2049,10666,10667,10671,10674],{},[2064,10668,10556,10669],{},[38,10670,3597],{},[2064,10672,10673],{},"Nuxt 3 (Vue)",[2064,10675,10676],{},"Next.js (React 19)",[2049,10678,10679,10684,10686],{},[2064,10680,10681,10682],{},"🖥️ ",[38,10683,2162],{},[2064,10685,3540],{},[2064,10687,10688],{},"Go + Gin",[2049,10690,10691,10697,10700],{},[2064,10692,10693,10694],{},"📄 ",[38,10695,10696],{},"内容来源",[2064,10698,10699],{},"Markdown 文件",[2064,10701,10702],{},"API 数据库",[2049,10704,10705,10710,10713],{},[2064,10706,10616,10707],{},[38,10708,10709],{},"发布方式",[2064,10711,10712],{},"全量重新构建",[2064,10714,10715],{},"API 实时发布",[2049,10717,10718,10723,10726],{},[2064,10719,10485,10720],{},[38,10721,10722],{},"用户系统",[2064,10724,10725],{},"❌ 无",[2064,10727,10728],{},"✅ 注册\u002F登录",[2049,10730,10731,10737,10740],{},[2064,10732,10733,10734],{},"💬 ",[38,10735,10736],{},"评论",[2064,10738,10739],{},"Giscus（需 GitHub）",[2064,10741,10742],{},"自有评论系统（规划中）",[2049,10744,10745,10749,10752],{},[2064,10746,10570,10747],{},[38,10748,3906],{},[2064,10750,10751],{},"1 个静态目录",[2064,10753,10754],{},"前端静态 + 后端二进制",[2049,10756,10757,10763,10766],{},[2064,10758,10759,10760],{},"⚡ ",[38,10761,10762],{},"更新体验",[2064,10764,10765],{},"改文章 → 构建 → 部署",[2064,10767,10768],{},"改文章 → 立即生效",[11,10770,10772],{"id":10771},"新功能注册账号","🎉 新功能：注册账号",[16,10774,10775,10776,10779],{},"伴随着架构升级，",[38,10777,10778],{},"用户账号系统","也上线了。",[73,10781,10783],{"id":10782},"目前支持","📋 目前支持",[16,10785,10786,10787,5279],{},"现在你可以在尼克的小窝 ",[38,10788,10789],{},"注册一个账号",[78,10791,10794],{"className":10792,"code":10793,"language":3474},[3472],"1. ✏️ 填写用户名、邮箱、密码\n2. 📧 查收激活邮件\n3. 🔗 点击链接激活账号\n4. 🚪 登录后查看和管理个人信息\n",[30,10795,10793],{"__ignoreMap":83},[11,10797,10799],{"id":10798},"️-后续计划","🗓️ 后续计划",[16,10801,10802],{},"正在开发或规划中的功能：",[3705,10804,10806,10812,10818,10824,10830,10836,10842,10848],{"className":10805},[3939],[51,10807,10809,10811],{"className":10808},[3943],[3945,10810],{"checked":141,"disabled":141,"type":3947}," 用户注册\u002F登录（已上线 🎉）",[51,10813,10815,10817],{"className":10814},[3943],[3945,10816],{"checked":141,"disabled":141,"type":3947}," 个人资料编辑（已上线 🎉）",[51,10819,10821,10823],{"className":10820},[3943],[3945,10822],{"disabled":141,"type":3947}," 💬 站内评论系统",[51,10825,10827,10829],{"className":10826},[3943],[3945,10828],{"disabled":141,"type":3947}," 🔖 文章收藏",[51,10831,10833,10835],{"className":10832},[3943],[3945,10834],{"disabled":141,"type":3947}," 📚 阅读历史",[51,10837,10839,10841],{"className":10838},[3943],[3945,10840],{"disabled":141,"type":3947}," 👍 文章点赞",[51,10843,10845,10847],{"className":10844},[3943],[3945,10846],{"disabled":141,"type":3947}," 🔔 关注通知",[51,10849,10851,10853],{"className":10850},[3943],[3945,10852],{"disabled":141,"type":3947}," 🎨 个人首页定制",[11,10855,10857],{"id":10856},"结语","💡 结语",[16,10859,10860,10861,10864],{},"从 Nuxt 3 纯静态到 React + Go 前后端分离，SuBlog 的架构变迁本质上是一个",[38,10862,10863],{},"功能驱动","的过程。静态博客有它的位置——写写文章、展示内容完全够用。但当你想做的不仅仅是\"展示\"时，后端的能力就不可或缺了。",[16,10866,10867,10868,10871],{},"这次重构最大的感触是：",[38,10869,10870],{},"架构选型不是越复杂越好，而是刚好够用最好","。纯静态阶段用 Nuxt 3 一点问题没有，直到功能需求超出了静态的边界，才自然推动到前后端分离。",[16,10873,10874,10875],{},"如果你也在纠结\"要不要上后端\"，不妨先问问自己：",[38,10876,10877],{},"你要做的是\"展示内容\"还是\"提供服务\"？",[16,10879,10880],{},"前者，静态就够了；后者，来吧，后端欢迎你。😄",[3982,10882],{},[16,10884,10885],{},[38,10886,10887],{},"🎉 目前注册功能已经可以用了，后续更多好玩的功能正在路上。部分功能可能需要登录才能体验，快注册一个账号吧！",{"title":83,"searchDepth":104,"depth":145,"links":10889},[10890,10891,10897,10902,10903,10906,10907],{"id":10368,"depth":104,"text":10369},{"id":10386,"depth":104,"text":10387,"children":10892},[10893,10894,10895,10896],{"id":10390,"depth":145,"text":10391},{"id":10400,"depth":145,"text":10401},{"id":4819,"depth":145,"text":10435},{"id":10452,"depth":145,"text":10453},{"id":10527,"depth":104,"text":10528,"children":10898},[10899,10900,10901],{"id":10531,"depth":145,"text":10391},{"id":10540,"depth":145,"text":10541},{"id":10587,"depth":145,"text":10588},{"id":10646,"depth":104,"text":10647},{"id":10771,"depth":104,"text":10772,"children":10904},[10905],{"id":10782,"depth":145,"text":10783},{"id":10798,"depth":104,"text":10799},{"id":10856,"depth":104,"text":10857},"记录尼克的小窝从 Nuxt 3 纯静态博客演进到 React + Go 前后端分离架构的全过程，包括架构设计的思考、技术选型的变化，以及新增的账号注册功能。","\u002Fposts\u002Fsublog-evolution\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsublog-evolution","2026-07-23T12:00:00",{"title":10363,"description":10908},"尼克的小窝 开发记录","posts\u002Fsublog-evolution\u002Findex",[10917,10918,3501,2158],"架构","React","2026-07-23","VMQ6A2K8GQ5t3DSlezWPd3KgXnmDVMn9QVeaI28Hcp8",{"id":10922,"title":10923,"author":6,"body":10924,"description":12087,"difficulty":3367,"draft":2148,"extension":2149,"image":12088,"meta":12089,"navigation":141,"path":12090,"pinned":2148,"published":12091,"seo":12092,"series":6,"seriesOrder":6,"stem":12093,"tags":12094,"updated":12098,"__hash__":12099},"posts\u002Fposts\u002Fip-geolocation-guide\u002Findex.md","IP 属地是怎么查出来的？本地查询与在线查询方案对比",{"type":8,"value":10925,"toc":12063},[10926,10928,10934,10937,10948,10958,10962,10968,10981,10987,10998,11005,11008,11015,11019,11030,11034,11045,11048,11052,11058,11064,11067,11071,11078,11096,11099,11102,11106,11137,11176,11182,11186,11325,11361,11370,11374,11414,11419,11423,11434,11437,11471,11474,11478,11481,11485,11676,11680,11763,11767,11774,11881,11885,11894,11980,11983,11986,12022,12025,12028,12036,12057,12060],[11,10927,10368],{"id":10368},[16,10929,10930,10931],{},"很多网站会在「登录记录」「安全中心」里显示你的登录 IP 归属地，比如「福建·福州 · 中国移动」。你可能好奇过：",[38,10932,10933],{},"服务器是怎么根据一个 IP 就判断出我在哪个城市、哪个运营商的？",[16,10935,10936],{},"这篇科普文章讲清楚三件事：",[48,10938,10939,10942,10945],{},[51,10940,10941],{},"IP 属地到底查的是什么；",[51,10943,10944],{},"纯本地查询为什么能做到毫秒级且不联网；",[51,10946,10947],{},"几种主流查询方案的优缺点对比，方便你按场景选型。",[10949,10950,10951],"blockquote",{},[16,10952,10953,10954,10957],{},"说明：文中用到的 IP 均为 RFC 5737 保留的文档示例段（TEST-NET-2，",[30,10955,10956],{},"198.51.100.0\u002F24","），不会对应任何真实地址。",[11,10959,10961],{"id":10960},"先想清楚ip-属地查的是什么","先想清楚：IP 属地查的是什么",[16,10963,10964,10965,1178],{},"一个常见误区是「每个 IP 对应一个具体地址」。实际上 IP 属地的本质是 ",[38,10966,10967],{},"IP 段与归属地的映射",[16,10969,10970,10971,5279],{},"运营商和机构在申请 IP 时，是按",[38,10972,10973,10974,10980],{},"网段（CIDR）",[38,10975,10976,10977,10979],{},"成块分配的。比如 ",[30,10978,10956],{}," 这一个段，一整块都归属同一地区、同一运营商。所以归属地数据天然可以","按段存储",[78,10982,10985],{"className":10983,"code":10984,"language":3474,"meta":83},[3472],"IP 起始          IP 结束         归属地\n────────────────────────────────────────\n198.51.100.0     198.51.100.255  中国|福建|福州|移动\n198.51.101.0     198.51.101.255  中国|浙江|杭州|电信\n",[30,10986,10984],{"__ignoreMap":83},[16,10988,10989,10990,10993,10994,10997],{},"全球公网 IP 约有 42 亿个，但",[38,10991,10992],{},"网段只有几十万条","。正因为存的是「段」而不是「每个 IP」，完整的归属地数据文件才只有几 MB（ip2region 的 ",[30,10995,10996],{},".xdb"," 约 11MB）。这是「本地离线查询」可行的第一前提。",[16,10999,11000,11001,11004],{},"所以，",[38,11002,11003],{},"IP 属地从来不是精确到街道的定位","，它返回的是段级的「省｜市｜运营商」粗粒度信息。",[11,11006,11007],{"id":11007},"纯本地查询的原理",[16,11009,11010,11011,11014],{},"以开源的 ",[38,11012,11013],{},"ip2region"," 为例，它的查询完全在本地完成，不联网、不请求第三方，几十微秒到毫秒级返回。",[73,11016,11018],{"id":11017},"第一步把-ip-变成整数","第一步：把 IP 变成整数",[16,11020,11021,11022,11025,11026,11029],{},"以 ",[30,11023,11024],{},"198.51.100.42"," 为例，它会转成一个 32 位整数 ",[30,11027,11028],{},"0xC633642A","。IP 的四个数字本质就是 4 个字节，拼起来就是一个整数，这一步没有任何玄学。",[73,11031,11033],{"id":11032},"第二步二分查找","第二步：二分查找",[16,11035,11036,11037,11040,11041,11044],{},"在按起始 IP 排序的段列表里做",[38,11038,11039],{},"二分查找","，找到满足 ",[30,11042,11043],{},"段的起始IP ≤ 目标整数 ≤ 段的结束IP"," 的那一段。",[16,11046,11047],{},"二分查找的复杂度是 O(log N)，N 是几十万条，所以很快——这就是「不靠网络也能毫秒返回」的核心。",[73,11049,11051],{"id":11050},"第三步xdb-文件的两级结构","第三步：.xdb 文件的两级结构",[16,11053,11054,11055,11057],{},"ip2region 的 ",[30,11056,10996],{}," 不是简单顺序文件，内部分两个区，进一步加速：",[78,11059,11062],{"className":11060,"code":11061,"language":3474,"meta":83},[3472],"┌─────────────────────────────┐\n│  索引区 (Index)             │  \u002F\u002F 每段一条定长记录：startIP \u002F endIP \u002F 数据区指针 + 数据长度\n│  …… 二分快速定位段 ……       │\n├─────────────────────────────┤\n│  数据区 (Data)              │  \u002F\u002F 真正存放各段的地点字符串\n│  ……                         │\n└─────────────────────────────┘\n",[30,11063,11061],{"__ignoreMap":83},[16,11065,11066],{},"先二分索引区定位到「是哪一段」，再按指针去数据区读出地点字符串，一次磁盘读就能拿到结果。",[73,11068,11070],{"id":11069},"为什么离线数据不会很快过期","为什么「离线数据」不会很快过期",[16,11072,11073,11074,11077],{},"IP 归属取决于",[38,11075,11076],{},"注册机构和运营商的分配","，一个网段通常长期稳定属于某地区、某运营商。所以：",[3705,11079,11080,11083,11093],{},[51,11081,11082],{},"下载一次数据文件，可以用很久；",[51,11084,11085,11086,11092],{},"归属偶尔变化时，",[38,11087,11088,11089,11091],{},"每月更新一次 ",[30,11090,10996],{}," 文件","即可（替换文件、不用重启服务，见下文实战）；",[51,11094,11095],{},"数据源来自 IANA\u002FAPNIC 等机构的分配表与公开归属信息，不是实时探测。",[11,11097,11098],{"id":11098},"主流查询方案对比",[16,11100,11101],{},"实际开发中，查询 IP 属地主要有三条路线。",[73,11103,11105],{"id":11104},"方案一ip2region纯本地离线推荐","方案一：ip2region（纯本地离线，推荐）",[78,11107,11109],{"className":118,"code":11108,"language":94,"meta":83,"style":83},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\nregion, err := searcher.SearchByStr(ip)\n\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[30,11110,11111,11116,11132],{"__ignoreMap":83},[87,11112,11113],{"class":89,"line":90},[87,11114,11115],{"class":173},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\n",[87,11117,11118,11121,11123,11126,11129],{"class":89,"line":104},[87,11119,11120],{"class":148},"region, err ",[87,11122,152],{"class":126},[87,11124,11125],{"class":148}," searcher.",[87,11127,11128],{"class":93},"SearchByStr",[87,11130,11131],{"class":148},"(ip)\n",[87,11133,11134],{"class":89,"line":145},[87,11135,11136],{"class":173},"\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[2043,11138,11139,11147],{},[2046,11140,11141],{},[2049,11142,11143,11145],{},[2052,11144,4819],{},[2052,11146,4822],{},[2059,11148,11149,11157,11165],{},[2049,11150,11151,11154],{},[2064,11152,11153],{},"免费、无网络依赖、无 QPS 限制",[2064,11155,11156],{},"段级粒度，个别段可能显示到相邻地区",[2049,11158,11159,11162],{},[2064,11160,11161],{},"毫秒级查询，永不因外部服务挂掉",[2064,11163,11164],{},"数据按月更新，最新分配的段可能短暂缺失",[2049,11166,11167,11170],{},[2064,11168,11169],{},"数据文件可随版本更新，不重启",[2064,11171,11172,11173],{},"只返回省市区+运营商，无经纬度；",[38,11174,11175],{},"仅支持 IPv4",[16,11177,11178,11181],{},[38,11179,11180],{},"适用","：博客\u002F后台的「登录记录显示归属地」这类展示用途。",[73,11183,11185],{"id":11184},"方案二在线-apiip-api-高德-腾讯位置服务","方案二：在线 API（ip-api \u002F 高德 \u002F 腾讯位置服务）",[78,11187,11189],{"className":118,"code":11188,"language":94,"meta":83,"style":83},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\ndefer cancel()\nreq, _ := http.NewRequestWithContext(ctx, \"GET\", \"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\", nil)\nresp, err := http.DefaultClient.Do(req)\nif err != nil {\n    return \"未知\" \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n}\ndefer resp.Body.Close()\n\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[30,11190,11191,11196,11224,11234,11265,11281,11294,11304,11308,11320],{"__ignoreMap":83},[87,11192,11193],{"class":89,"line":90},[87,11194,11195],{"class":173},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\n",[87,11197,11198,11201,11203,11205,11208,11211,11214,11217,11219,11221],{"class":89,"line":104},[87,11199,11200],{"class":148},"ctx, cancel ",[87,11202,152],{"class":126},[87,11204,1674],{"class":148},[87,11206,11207],{"class":93},"WithTimeout",[87,11209,11210],{"class":148},"(context.",[87,11212,11213],{"class":93},"Background",[87,11215,11216],{"class":148},"(), ",[87,11218,4101],{"class":415},[87,11220,371],{"class":126},[87,11222,11223],{"class":148},"time.Second)\n",[87,11225,11226,11229,11232],{"class":89,"line":145},[87,11227,11228],{"class":126},"defer",[87,11230,11231],{"class":93}," cancel",[87,11233,805],{"class":148},[87,11235,11236,11239,11241,11244,11247,11250,11253,11255,11258,11260,11263],{"class":89,"line":164},[87,11237,11238],{"class":148},"req, _ ",[87,11240,152],{"class":126},[87,11242,11243],{"class":148}," http.",[87,11245,11246],{"class":93},"NewRequestWithContext",[87,11248,11249],{"class":148},"(ctx, ",[87,11251,11252],{"class":97},"\"GET\"",[87,11254,354],{"class":148},[87,11256,11257],{"class":97},"\"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\"",[87,11259,354],{"class":148},[87,11261,11262],{"class":415},"nil",[87,11264,231],{"class":148},[87,11266,11267,11270,11272,11275,11278],{"class":89,"line":177},[87,11268,11269],{"class":148},"resp, err ",[87,11271,152],{"class":126},[87,11273,11274],{"class":148}," http.DefaultClient.",[87,11276,11277],{"class":93},"Do",[87,11279,11280],{"class":148},"(req)\n",[87,11282,11283,11286,11288,11290,11292],{"class":89,"line":189},[87,11284,11285],{"class":126},"if",[87,11287,586],{"class":148},[87,11289,589],{"class":126},[87,11291,592],{"class":415},[87,11293,595],{"class":148},[87,11295,11296,11298,11301],{"class":89,"line":201},[87,11297,399],{"class":126},[87,11299,11300],{"class":97}," \"未知\"",[87,11302,11303],{"class":173}," \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n",[87,11305,11306],{"class":89,"line":211},[87,11307,1062],{"class":148},[87,11309,11310,11312,11315,11318],{"class":89,"line":228},[87,11311,11228],{"class":126},[87,11313,11314],{"class":148}," resp.Body.",[87,11316,11317],{"class":93},"Close",[87,11319,805],{"class":148},[87,11321,11322],{"class":89,"line":390},[87,11323,11324],{"class":173},"\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[2043,11326,11327,11335],{},[2046,11328,11329],{},[2049,11330,11331,11333],{},[2052,11332,4819],{},[2052,11334,4822],{},[2059,11336,11337,11345,11353],{},[2049,11338,11339,11342],{},[2064,11340,11341],{},"数据最新、可能更精确",[2064,11343,11344],{},"有频率限制（免费版限 QPS）",[2049,11346,11347,11350],{},[2064,11348,11349],{},"部分服务还返回经纬度\u002F时区",[2064,11351,11352],{},"依赖第三方，断网\u002F被墙\u002F限流时拿不到",[2049,11354,11355,11358],{},[2064,11356,11357],{},"无需维护数据文件",[2064,11359,11360],{},"每次查询都要发一次 HTTP 请求",[16,11362,11363,11365,11366,11369],{},[38,11364,11180],{},"：对精度要求高、且能接受外部依赖与限流的场景。",[38,11367,11368],{},"务必加超时和降级","，否则第三方一抖，你自己的页面就卡住。",[73,11371,11373],{"id":11372},"方案三maxmind-geolite2离线-mmdb","方案三：MaxMind GeoLite2（离线 .mmdb）",[2043,11375,11376,11384],{},[2046,11377,11378],{},[2049,11379,11380,11382],{},[2052,11381,4819],{},[2052,11383,4822],{},[2059,11385,11386,11394,11406],{},[2049,11387,11388,11391],{},[2064,11389,11390],{},"全球数据，覆盖广、精度较好",[2064,11392,11393],{},"免费但需注册账号并遵守 EULA；商用需 GeoIP2 商业授权",[2049,11395,11396,11403],{},[2064,11397,11398,11399,11402],{},"标准 ",[30,11400,11401],{},".mmdb"," 格式，各语言都有库，支持 IPv6",[2064,11404,11405],{},"国内 IP 的省市粒度有时不如 ip2region 细",[2049,11407,11408,11411],{},[2064,11409,11410],{},"离线查询",[2064,11412,11413],{},"数据同样要定期更新",[16,11415,11416,11418],{},[38,11417,11180],{},"：面向全球用户的场景。",[11,11420,11422],{"id":11421},"ipv6本地离线方案的一个盲点","IPv6：本地离线方案的一个盲点",[16,11424,11425,11426,11429,11430,11433],{},"前面聊的都是 IPv4（32 位）。但家宽和移动网络越来越多地",[38,11427,11428],{},"只下发 IPv6","（128 位）地址，而 ",[38,11431,11432],{},"ip2region 目前只支持 IPv4","——拿到一个纯 IPv6 地址是查不出归属地的，本地库会返回空结果。",[16,11435,11436],{},"实际影响与常见应对：",[48,11438,11439,11449,11455,11465],{},[51,11440,11441,11444,11445,11448],{},[38,11442,11443],{},"判断版本","：Go 里 ",[30,11446,11447],{},"net.ParseIP(ip).To4() != nil"," 就是 IPv4，先判断再查询；",[51,11450,11451,11454],{},[38,11452,11453],{},"降级展示","：IPv6 直接显示「IPv6 地址」，不显示属地，诚实第一；",[51,11456,11457,11460,11461,11464],{},[38,11458,11459],{},"优先取 IPv4","：有反向代理时，从 ",[30,11462,11463],{},"X-Forwarded-For"," 里挑一个 IPv4 来查；",[51,11466,11467,11470],{},[38,11468,11469],{},"需要 v6 归属","：换支持 IPv6 的数据源（如 MaxMind GeoLite2）。",[16,11472,11473],{},"这一点在接入时很容易漏掉，上线前记得拿一个纯 IPv6 地址回归一遍。",[11,11475,11477],{"id":11476},"实战给登录记录加-ip-属地","实战：给登录记录加 IP 属地",[16,11479,11480],{},"subLog 博客的登录记录就用了 ip2region。完整接入一共四步。",[73,11482,11484],{"id":11483},"_1-进程启动时加载数据文件","1. 进程启动时加载数据文件",[78,11486,11488],{"className":118,"code":11487,"language":94,"meta":83,"style":83},"import (\n    \"sync\u002Fatomic\"\n    \"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb\"\n)\n\n\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\nvar searcher atomic.Pointer[xdb.Searcher]\n\nfunc initSearcher() error {\n    content, err := os.ReadFile(\".\u002Fdata\u002Fip2region.xdb\") \u002F\u002F 约 11MB，一次性读入内存\n    if err != nil {\n        return err\n    }\n    s, err := xdb.NewWithBuffer(content)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s)\n    return nil\n}\n",[30,11489,11490,11496,11505,11514,11518,11522,11527,11554,11558,11571,11593,11605,11612,11616,11632,11644,11650,11654,11665,11672],{"__ignoreMap":83},[87,11491,11492,11494],{"class":89,"line":90},[87,11493,127],{"class":126},[87,11495,1578],{"class":148},[87,11497,11498,11500,11503],{"class":89,"line":104},[87,11499,8043],{"class":97},[87,11501,11502],{"class":93},"sync\u002Fatomic",[87,11504,136],{"class":97},[87,11506,11507,11509,11512],{"class":89,"line":145},[87,11508,8043],{"class":97},[87,11510,11511],{"class":93},"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb",[87,11513,136],{"class":97},[87,11515,11516],{"class":89,"line":164},[87,11517,231],{"class":148},[87,11519,11520],{"class":89,"line":177},[87,11521,142],{"emptyLinePlaceholder":141},[87,11523,11524],{"class":89,"line":189},[87,11525,11526],{"class":173},"\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\n",[87,11528,11529,11531,11534,11537,11539,11542,11544,11547,11549,11552],{"class":89,"line":201},[87,11530,8124],{"class":126},[87,11532,11533],{"class":148}," searcher ",[87,11535,11536],{"class":93},"atomic",[87,11538,348],{"class":148},[87,11540,11541],{"class":93},"Pointer",[87,11543,4947],{"class":148},[87,11545,11546],{"class":93},"xdb",[87,11548,348],{"class":148},[87,11550,11551],{"class":93},"Searcher",[87,11553,2615],{"class":148},[87,11555,11556],{"class":89,"line":211},[87,11557,142],{"emptyLinePlaceholder":141},[87,11559,11560,11562,11565,11567,11569],{"class":89,"line":228},[87,11561,336],{"class":126},[87,11563,11564],{"class":93}," initSearcher",[87,11566,1622],{"class":148},[87,11568,384],{"class":126},[87,11570,595],{"class":148},[87,11572,11573,11576,11578,11581,11583,11585,11588,11590],{"class":89,"line":390},[87,11574,11575],{"class":148},"    content, err ",[87,11577,152],{"class":126},[87,11579,11580],{"class":148}," os.",[87,11582,8263],{"class":93},[87,11584,219],{"class":148},[87,11586,11587],{"class":97},"\".\u002Fdata\u002Fip2region.xdb\"",[87,11589,1208],{"class":148},[87,11591,11592],{"class":173},"\u002F\u002F 约 11MB，一次性读入内存\n",[87,11594,11595,11597,11599,11601,11603],{"class":89,"line":396},[87,11596,583],{"class":126},[87,11598,586],{"class":148},[87,11600,589],{"class":126},[87,11602,592],{"class":415},[87,11604,595],{"class":148},[87,11606,11607,11609],{"class":89,"line":419},[87,11608,600],{"class":126},[87,11610,11611],{"class":148}," err\n",[87,11613,11614],{"class":89,"line":617},[87,11615,620],{"class":148},[87,11617,11618,11621,11623,11626,11629],{"class":89,"line":623},[87,11619,11620],{"class":148},"    s, err ",[87,11622,152],{"class":126},[87,11624,11625],{"class":148}," xdb.",[87,11627,11628],{"class":93},"NewWithBuffer",[87,11630,11631],{"class":148},"(content)\n",[87,11633,11634,11636,11638,11640,11642],{"class":89,"line":629},[87,11635,583],{"class":126},[87,11637,586],{"class":148},[87,11639,589],{"class":126},[87,11641,592],{"class":415},[87,11643,595],{"class":148},[87,11645,11646,11648],{"class":89,"line":647},[87,11647,600],{"class":126},[87,11649,11611],{"class":148},[87,11651,11652],{"class":89,"line":927},[87,11653,620],{"class":148},[87,11655,11656,11659,11662],{"class":89,"line":932},[87,11657,11658],{"class":148},"    searcher.",[87,11660,11661],{"class":93},"Store",[87,11663,11664],{"class":148},"(s)\n",[87,11666,11667,11669],{"class":89,"line":951},[87,11668,399],{"class":126},[87,11670,11671],{"class":415}," nil\n",[87,11673,11674],{"class":89,"line":956},[87,11675,1062],{"class":148},[73,11677,11679],{"id":11678},"_2-查询一次","2. 查询一次",[78,11681,11683],{"className":118,"code":11682,"language":94,"meta":83,"style":83},"func ipRegion(ip string) string {\n    region, err := searcher.Load().SearchByStr(ip)\n    if err != nil {\n        return \"未知\"\n    }\n    return region \u002F\u002F \"中国|福建省|福州市|移动\"\n}\n",[30,11684,11685,11707,11726,11738,11745,11749,11759],{"__ignoreMap":83},[87,11686,11687,11689,11692,11694,11697,11700,11702,11705],{"class":89,"line":90},[87,11688,336],{"class":126},[87,11690,11691],{"class":93}," ipRegion",[87,11693,219],{"class":148},[87,11695,11696],{"class":341},"ip",[87,11698,11699],{"class":126}," string",[87,11701,1208],{"class":148},[87,11703,11704],{"class":126},"string",[87,11706,595],{"class":148},[87,11708,11709,11712,11714,11716,11719,11722,11724],{"class":89,"line":104},[87,11710,11711],{"class":148},"    region, err ",[87,11713,152],{"class":126},[87,11715,11125],{"class":148},[87,11717,11718],{"class":93},"Load",[87,11720,11721],{"class":148},"().",[87,11723,11128],{"class":93},[87,11725,11131],{"class":148},[87,11727,11728,11730,11732,11734,11736],{"class":89,"line":145},[87,11729,583],{"class":126},[87,11731,586],{"class":148},[87,11733,589],{"class":126},[87,11735,592],{"class":415},[87,11737,595],{"class":148},[87,11739,11740,11742],{"class":89,"line":164},[87,11741,600],{"class":126},[87,11743,11744],{"class":97}," \"未知\"\n",[87,11746,11747],{"class":89,"line":177},[87,11748,620],{"class":148},[87,11750,11751,11753,11756],{"class":89,"line":189},[87,11752,399],{"class":126},[87,11754,11755],{"class":148}," region ",[87,11757,11758],{"class":173},"\u002F\u002F \"中国|福建省|福州市|移动\"\n",[87,11760,11761],{"class":89,"line":201},[87,11762,1062],{"class":148},[73,11764,11766],{"id":11765},"_3-加一层内存缓存","3. 加一层内存缓存",[16,11768,11769,11770,11773],{},"登录记录里同一个 IP 可能反复出现，用 ",[30,11771,11772],{},"sync.Map"," 按 IP 缓存结果，一次查询终身复用：",[78,11775,11777],{"className":118,"code":11776,"language":94,"meta":83,"style":83},"var regionCache sync.Map\n\nfunc ipRegionCached(ip string) string {\n    if v, ok := regionCache.Load(ip); ok {\n        return v.(string)\n    }\n    region := ipRegion(ip)\n    regionCache.Store(ip, region)\n    return region\n}\n",[30,11778,11779,11794,11798,11817,11834,11845,11849,11860,11870,11877],{"__ignoreMap":83},[87,11780,11781,11783,11786,11789,11791],{"class":89,"line":90},[87,11782,8124],{"class":126},[87,11784,11785],{"class":148}," regionCache ",[87,11787,11788],{"class":93},"sync",[87,11790,348],{"class":148},[87,11792,11793],{"class":93},"Map\n",[87,11795,11796],{"class":89,"line":104},[87,11797,142],{"emptyLinePlaceholder":141},[87,11799,11800,11802,11805,11807,11809,11811,11813,11815],{"class":89,"line":145},[87,11801,336],{"class":126},[87,11803,11804],{"class":93}," ipRegionCached",[87,11806,219],{"class":148},[87,11808,11696],{"class":341},[87,11810,11699],{"class":126},[87,11812,1208],{"class":148},[87,11814,11704],{"class":126},[87,11816,595],{"class":148},[87,11818,11819,11821,11824,11826,11829,11831],{"class":89,"line":164},[87,11820,583],{"class":126},[87,11822,11823],{"class":148}," v, ok ",[87,11825,152],{"class":126},[87,11827,11828],{"class":148}," regionCache.",[87,11830,11718],{"class":93},[87,11832,11833],{"class":148},"(ip); ok {\n",[87,11835,11836,11838,11841,11843],{"class":89,"line":177},[87,11837,600],{"class":126},[87,11839,11840],{"class":148}," v.(",[87,11842,11704],{"class":126},[87,11844,231],{"class":148},[87,11846,11847],{"class":89,"line":189},[87,11848,620],{"class":148},[87,11850,11851,11854,11856,11858],{"class":89,"line":201},[87,11852,11853],{"class":148},"    region ",[87,11855,152],{"class":126},[87,11857,11691],{"class":93},[87,11859,11131],{"class":148},[87,11861,11862,11865,11867],{"class":89,"line":211},[87,11863,11864],{"class":148},"    regionCache.",[87,11866,11661],{"class":93},[87,11868,11869],{"class":148},"(ip, region)\n",[87,11871,11872,11874],{"class":89,"line":228},[87,11873,399],{"class":126},[87,11875,11876],{"class":148}," region\n",[87,11878,11879],{"class":89,"line":390},[87,11880,1062],{"class":148},[73,11882,11884],{"id":11883},"_4-数据热更新不用重启","4. 数据热更新（不用重启）",[16,11886,11887,11888,11890,11891,11893],{},"ip2region 每月发布新数据。下载新文件后重新 ",[30,11889,11628],{}," 并 ",[30,11892,11661],{}," 覆盖即可，进程完全不用重启：",[78,11895,11897],{"className":118,"code":11896,"language":94,"meta":83,"style":83},"func reloadSearcher(newContent []byte) error {\n    s, err := xdb.NewWithBuffer(newContent)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s) \u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n    return nil\n}\n",[30,11898,11899,11923,11936,11948,11954,11958,11970,11976],{"__ignoreMap":83},[87,11900,11901,11903,11906,11908,11911,11914,11917,11919,11921],{"class":89,"line":90},[87,11902,336],{"class":126},[87,11904,11905],{"class":93}," reloadSearcher",[87,11907,219],{"class":148},[87,11909,11910],{"class":341},"newContent",[87,11912,11913],{"class":148}," []",[87,11915,11916],{"class":126},"byte",[87,11918,1208],{"class":148},[87,11920,384],{"class":126},[87,11922,595],{"class":148},[87,11924,11925,11927,11929,11931,11933],{"class":89,"line":104},[87,11926,11620],{"class":148},[87,11928,152],{"class":126},[87,11930,11625],{"class":148},[87,11932,11628],{"class":93},[87,11934,11935],{"class":148},"(newContent)\n",[87,11937,11938,11940,11942,11944,11946],{"class":89,"line":145},[87,11939,583],{"class":126},[87,11941,586],{"class":148},[87,11943,589],{"class":126},[87,11945,592],{"class":415},[87,11947,595],{"class":148},[87,11949,11950,11952],{"class":89,"line":164},[87,11951,600],{"class":126},[87,11953,11611],{"class":148},[87,11955,11956],{"class":89,"line":177},[87,11957,620],{"class":148},[87,11959,11960,11962,11964,11967],{"class":89,"line":189},[87,11961,11658],{"class":148},[87,11963,11661],{"class":93},[87,11965,11966],{"class":148},"(s) ",[87,11968,11969],{"class":173},"\u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n",[87,11971,11972,11974],{"class":89,"line":201},[87,11973,399],{"class":126},[87,11975,11671],{"class":415},[87,11977,11978],{"class":89,"line":211},[87,11979,1062],{"class":148},[11,11981,11982],{"id":11982},"局限与诚实提醒",[16,11984,11985],{},"无论用哪种方案，都要清楚 IP 属地的边界：",[3705,11987,11988,11994,12000,12006,12012],{},[51,11989,11990,11993],{},[38,11991,11992],{},"段级精度","：网段归属是「绝大多数」准确，个别 IP（大型企业自建段、跨省调度段）可能显示到相邻地区或只到省。",[51,11995,11996,11999],{},[38,11997,11998],{},"运营商级，不是定位","：拿到的是「省｜市｜运营商」，不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位，纯 IP 归属做不到。",[51,12001,12002,12005],{},[38,12003,12004],{},"动态 IP","：家宽通常是动态 IP，重拨会变，所以「上次登录在福州，这次在广州」不一定是异常，可能是 IP 变了。",[51,12007,12008,12011],{},[38,12009,12010],{},"别用于安全判定","：用 IP 段做封禁或风控判定风险自负，段级数据不适合当精确依据。",[51,12013,12014,12017,12018,12021],{},[38,12015,12016],{},"隐私与合规","：IP 地址属于个人信息（《个人信息保护法》）。归属地展示建议只出现在",[38,12019,12020],{},"用户本人可见","的页面（登录记录、个人中心）；若在评论区等公开场合展示，建议只显示到省市并明确告知用户。",[11,12023,12024],{"id":12024},"小结",[16,12026,12027],{},"一句话总结：",[10949,12029,12030],{},[16,12031,12032,12035],{},[38,12033,12034],{},"IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。"," 它查的是网段归属，不是精确位置。",[3705,12037,12038,12044,12051],{},[51,12039,12040,12041,12043],{},"想省心、免费、稳定，用 ",[38,12042,11013],{}," 本地离线查询（注意 IPv6 盲点）；",[51,12045,12046,12047,12050],{},"想要最新最细、能接受外部依赖，用",[38,12048,12049],{},"在线 API","（记得超时降级）；",[51,12052,12053,12054,1178],{},"面向全球用户、要 IPv6 覆盖，用 ",[38,12055,12056],{},"MaxMind GeoLite2",[16,12058,12059],{},"对个人博客「登录记录显示归属地」这种场景，本地离线方案通常是性价比最高的选择。",[2124,12061,12062],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}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 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);}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":83,"searchDepth":104,"depth":145,"links":12064},[12065,12066,12067,12073,12078,12079,12085,12086],{"id":10368,"depth":104,"text":10368},{"id":10960,"depth":104,"text":10961},{"id":11007,"depth":104,"text":11007,"children":12068},[12069,12070,12071,12072],{"id":11017,"depth":145,"text":11018},{"id":11032,"depth":145,"text":11033},{"id":11050,"depth":145,"text":11051},{"id":11069,"depth":145,"text":11070},{"id":11098,"depth":104,"text":11098,"children":12074},[12075,12076,12077],{"id":11104,"depth":145,"text":11105},{"id":11184,"depth":145,"text":11185},{"id":11372,"depth":145,"text":11373},{"id":11421,"depth":104,"text":11422},{"id":11476,"depth":104,"text":11477,"children":12080},[12081,12082,12083,12084],{"id":11483,"depth":145,"text":11484},{"id":11678,"depth":145,"text":11679},{"id":11765,"depth":145,"text":11766},{"id":11883,"depth":145,"text":11884},{"id":11982,"depth":104,"text":11982},{"id":12024,"depth":104,"text":12024},"科普 IP 属地查询的原理：IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级，对比 ip2region、在线 API 与 MaxMind 三种方案的优劣，并附登录记录场景的 Go 实战与 IPv6 盲点提醒。","\u002Fposts\u002Fip-geolocation-guide\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fip-geolocation-guide","2026-08-07T12:00:00",{"title":10923,"description":12087},"posts\u002Fip-geolocation-guide\u002Findex",[12095,12096,12097],"IP","网络","科普","2026-08-16","RY1UPgJaiKsjpC51uqMyG7w_GpCRS13_CX5j1hDD_dM",{"id":12101,"title":12102,"author":6,"body":12103,"description":13625,"difficulty":2147,"draft":2148,"extension":2149,"image":13626,"meta":13627,"navigation":141,"path":13628,"pinned":2148,"published":13629,"seo":13630,"series":2155,"seriesOrder":145,"stem":13631,"tags":13632,"updated":12098,"__hash__":13635},"posts\u002Fposts\u002Fgo-embed-react-single-binary\u002Findex.md","Go 单文件打包进阶：React 前端 + 交叉编译，一次踩完所有坑",{"type":8,"value":12104,"toc":13604},[12105,12107,12118,12129,12136,12139,12175,12177,12186,12188,12192,12202,12246,12288,12306,12310,12331,12362,12366,12658,12661,12699,12703,13046,13049,13053,13250,13260,13262,13273,13287,13300,13311,13315,13322,13338,13351,13356,13360,13376,13381,13426,13440,13451,13455,13470,13479,13501,13510,13514,13524,13526,13553,13562,13564,13567,13594,13601],[11,12106,4021],{"id":4021},[16,12108,12109,12110,12113,12114,12117],{},"这是「Go 单文件打包」系列第三篇。上一篇（",[3974,12111,12112],{"href":8942},"把前端塞进二进制","）记录了一个 Gin + Vue 3 项目的 embed 方案。这次换了个项目继续踩坑：",[38,12115,12116],{},"Magic Img","——一个自托管、本地优先的 AI 生图提示词管理工具，技术栈是：",[3705,12119,12120,12123],{},[51,12121,12122],{},"前端：React 19 + TypeScript + Vite + Tailwind v4（UI 库 Magic UI + shadcn\u002Fui）",[51,12124,12125,12126,7726],{},"后端：Go + Gin + GORM + SQLite（",[38,12127,12128],{},"modernc 纯 Go 驱动，无 cgo",[16,12130,12131,12132,12135],{},"目标一样：",[38,12133,12134],{},"一个二进制拖到任何机器上就能跑","——前端、API、数据库全部内嵌。",[16,12137,12138],{},"和上篇不同的地方有三个，正好是这篇要讲的重点：",[48,12140,12141,12155,12168],{},[51,12142,12143,12144,12150,12151,12154],{},"前端用了 ",[38,12145,12146,12147],{},"HashRouter + Vite base ",[30,12148,12149],{},"\u002Fdist","，服务端",[38,12152,12153],{},"不再需要 SPA fallback","（上篇坑一的新解法）；",[51,12156,12157,12158,12164,12165,12167],{},"embed 用 ",[38,12159,12160,12163],{},[30,12161,12162],{},"all:"," 前缀","一劳永逸解决 ",[30,12166,8636],{}," 开头文件被排除的问题（上篇坑二的升级解法）；",[51,12169,12170,12171,12174],{},"需要",[38,12172,12173],{},"在 Windows 上交叉编译 Linux 产物","，踩了「生成的文件无法在 Linux 执行」的坑。",[11,12176,7992],{"id":7992},[16,12178,12179,12180,12182,12183,12185],{},"和上篇一致：",[30,12181,7998],{}," 在编译期把前端 ",[30,12184,8002],{}," 塞进二进制，运行时由 Go 直接提供静态服务。差异在于路由策略和缓存策略，后面细说。",[11,12187,8006],{"id":8006},[73,12189,12191],{"id":12190},"_1-前端hash-路由-base-dist","1. 前端：hash 路由 + base \u002Fdist",[16,12193,12194,12195,12198,12199,5279],{},"vite.config.ts 里固定 ",[30,12196,12197],{},"base: \"\u002Fdist\u002F\"","，路由用 ",[30,12200,12201],{},"HashRouter",[78,12203,12205],{"className":8687,"code":12204,"language":8689,"meta":83,"style":83},"\u002F\u002F vite.config.ts\nexport default defineConfig({\n  base: \"\u002Fdist\u002F\",   \u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n  ...\n})\n",[30,12206,12207,12211,12223,12237,12242],{"__ignoreMap":83},[87,12208,12209],{"class":89,"line":90},[87,12210,8696],{"class":173},[87,12212,12213,12215,12217,12220],{"class":89,"line":104},[87,12214,2268],{"class":126},[87,12216,4880],{"class":126},[87,12218,12219],{"class":93}," defineConfig",[87,12221,12222],{"class":148},"({\n",[87,12224,12225,12228,12231,12234],{"class":89,"line":145},[87,12226,12227],{"class":148},"  base: ",[87,12229,12230],{"class":97},"\"\u002Fdist\u002F\"",[87,12232,12233],{"class":148},",   ",[87,12235,12236],{"class":173},"\u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n",[87,12238,12239],{"class":89,"line":164},[87,12240,12241],{"class":126},"  ...\n",[87,12243,12244],{"class":89,"line":177},[87,12245,422],{"class":148},[78,12247,12251],{"className":12248,"code":12249,"language":12250,"meta":83,"style":83},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n\u003CHashRouter>\n  \u003CRoutes>...\u003C\u002FRoutes>\n\u003C\u002FHashRouter>\n","tsx",[30,12252,12253,12258,12266,12280],{"__ignoreMap":83},[87,12254,12255],{"class":89,"line":90},[87,12256,12257],{"class":173},"\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n",[87,12259,12260,12262,12264],{"class":89,"line":104},[87,12261,3089],{"class":148},[87,12263,12201],{"class":415},[87,12265,2749],{"class":148},[87,12267,12268,12270,12273,12276,12278],{"class":89,"line":145},[87,12269,4712],{"class":148},[87,12271,12272],{"class":415},"Routes",[87,12274,12275],{"class":148},">...\u003C\u002F",[87,12277,12272],{"class":415},[87,12279,2749],{"class":148},[87,12281,12282,12284,12286],{"class":89,"line":164},[87,12283,4690],{"class":148},[87,12285,12201],{"class":415},[87,12287,2749],{"class":148},[16,12289,12290,12293,12294,12297,12298,12301,12302,12305],{},[38,12291,12292],{},"为什么这样选","：history 路由需要服务端把未知路径全部 fallback 回 ",[30,12295,12296],{},"index.html","（上篇坑一）。hash 路由下浏览器请求的永远是 ",[30,12299,12300],{},"\u002Fdist\u002F"," 这一个入口，",[38,12303,12304],{},"静态服务直接返回 index.html 就完事，一个 fallback 都不用写","。内嵌场景里这是最省心的组合。",[73,12307,12309],{"id":12308},"_2-构建产物复制进后端目录","2. 构建产物复制进后端目录",[16,12311,12312,12315,12316,12318,12319,12322,12323,12326,12327,12330],{},[30,12313,12314],{},"npm run build"," 除了编译，还会把 ",[30,12317,8002],{}," 复制到 ",[30,12320,12321],{},"backend\u002Fweb\u002Fdist","（一个小脚本 ",[30,12324,12325],{},"scripts\u002Fcopy-dist.mjs"," 干这个活），供 ",[30,12328,12329],{},"go:embed"," 使用：",[78,12332,12334],{"className":1968,"code":12333,"language":1970,"meta":83,"style":83},"\u002F\u002F frontend\u002Fpackage.json\n\"scripts\": {\n  \"build\": \"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n}\n",[30,12335,12336,12341,12348,12358],{"__ignoreMap":83},[87,12337,12338],{"class":89,"line":90},[87,12339,12340],{"class":173},"\u002F\u002F frontend\u002Fpackage.json\n",[87,12342,12343,12346],{"class":89,"line":104},[87,12344,12345],{"class":97},"\"scripts\"",[87,12347,1985],{"class":148},[87,12349,12350,12353,12355],{"class":89,"line":145},[87,12351,12352],{"class":415},"  \"build\"",[87,12354,1025],{"class":148},[87,12356,12357],{"class":97},"\"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n",[87,12359,12360],{"class":89,"line":164},[87,12361,1062],{"class":148},[73,12363,12365],{"id":12364},"_3-后端embed-httpfilesystem-包装","3. 后端：embed + http.FileSystem 包装",[78,12367,12369],{"className":118,"code":12368,"language":94,"meta":83,"style":83},"\u002F\u002F backend\u002Fweb\u002Fembed.go\npackage web\n\nimport (\n    \"embed\"\n    \"net\u002Fhttp\"\n    \"strings\"\n)\n\n\u002F\u002Fgo:embed all:dist\nvar distFS embed.FS\n\n\u002F\u002F Dist 内嵌前端静态文件系统\nvar Dist http.FileSystem = &fileSystem{prefix: \"dist\", fs: &distFS}\n\ntype fileSystem struct {\n    prefix string\n    fs     *embed.FS\n}\n\nfunc (f *fileSystem) Open(name string) (http.File, error) {\n    if name == \"\u002F\" {\n        name = f.prefix\n    } else {\n        name = f.prefix + \"\u002F\" + strings.TrimPrefix(name, \"\u002F\")\n    }\n    return http.FS(f.fs).Open(name)\n}\n",[30,12370,12371,12376,12382,12386,12392,12400,12408,12416,12420,12424,12429,12442,12446,12451,12488,12492,12504,12512,12525,12529,12533,12572,12586,12596,12606,12633,12637,12654],{"__ignoreMap":83},[87,12372,12373],{"class":89,"line":90},[87,12374,12375],{"class":173},"\u002F\u002F backend\u002Fweb\u002Fembed.go\n",[87,12377,12378,12380],{"class":89,"line":104},[87,12379,8025],{"class":126},[87,12381,8028],{"class":93},[87,12383,12384],{"class":89,"line":145},[87,12385,142],{"emptyLinePlaceholder":141},[87,12387,12388,12390],{"class":89,"line":164},[87,12389,127],{"class":126},[87,12391,1578],{"class":148},[87,12393,12394,12396,12398],{"class":89,"line":177},[87,12395,8043],{"class":97},[87,12397,8046],{"class":93},[87,12399,136],{"class":97},[87,12401,12402,12404,12406],{"class":89,"line":189},[87,12403,8043],{"class":97},[87,12405,8073],{"class":93},[87,12407,136],{"class":97},[87,12409,12410,12412,12414],{"class":89,"line":201},[87,12411,8043],{"class":97},[87,12413,8091],{"class":93},[87,12415,136],{"class":97},[87,12417,12418],{"class":89,"line":211},[87,12419,231],{"class":148},[87,12421,12422],{"class":89,"line":228},[87,12423,142],{"emptyLinePlaceholder":141},[87,12425,12426],{"class":89,"line":390},[87,12427,12428],{"class":173},"\u002F\u002Fgo:embed all:dist\n",[87,12430,12431,12433,12436,12438,12440],{"class":89,"line":396},[87,12432,8124],{"class":126},[87,12434,12435],{"class":148}," distFS ",[87,12437,8046],{"class":93},[87,12439,348],{"class":148},[87,12441,8134],{"class":93},[87,12443,12444],{"class":89,"line":419},[87,12445,142],{"emptyLinePlaceholder":141},[87,12447,12448],{"class":89,"line":617},[87,12449,12450],{"class":173},"\u002F\u002F Dist 内嵌前端静态文件系统\n",[87,12452,12453,12455,12458,12461,12463,12466,12468,12471,12474,12477,12479,12482,12485],{"class":89,"line":623},[87,12454,8124],{"class":126},[87,12456,12457],{"class":148}," Dist ",[87,12459,12460],{"class":93},"http",[87,12462,348],{"class":148},[87,12464,12465],{"class":93},"FileSystem",[87,12467,1589],{"class":126},[87,12469,12470],{"class":126}," &",[87,12472,12473],{"class":93},"fileSystem",[87,12475,12476],{"class":148},"{prefix: ",[87,12478,8174],{"class":97},[87,12480,12481],{"class":148},", fs: ",[87,12483,12484],{"class":126},"&",[87,12486,12487],{"class":148},"distFS}\n",[87,12489,12490],{"class":89,"line":629},[87,12491,142],{"emptyLinePlaceholder":141},[87,12493,12494,12496,12499,12502],{"class":89,"line":647},[87,12495,1564],{"class":126},[87,12497,12498],{"class":93}," fileSystem",[87,12500,12501],{"class":126}," struct",[87,12503,595],{"class":148},[87,12505,12506,12509],{"class":89,"line":927},[87,12507,12508],{"class":148},"    prefix ",[87,12510,12511],{"class":126},"string\n",[87,12513,12514,12517,12519,12521,12523],{"class":89,"line":932},[87,12515,12516],{"class":148},"    fs     ",[87,12518,371],{"class":126},[87,12520,8046],{"class":93},[87,12522,348],{"class":148},[87,12524,8134],{"class":93},[87,12526,12527],{"class":89,"line":951},[87,12528,1062],{"class":148},[87,12530,12531],{"class":89,"line":956},[87,12532,142],{"emptyLinePlaceholder":141},[87,12534,12535,12537,12539,12542,12544,12546,12548,12551,12553,12555,12557,12559,12561,12563,12566,12568,12570],{"class":89,"line":962},[87,12536,336],{"class":126},[87,12538,1817],{"class":148},[87,12540,12541],{"class":341},"f ",[87,12543,371],{"class":126},[87,12545,12473],{"class":93},[87,12547,1208],{"class":148},[87,12549,12550],{"class":93},"Open",[87,12552,219],{"class":148},[87,12554,8740],{"class":341},[87,12556,11699],{"class":126},[87,12558,368],{"class":148},[87,12560,12460],{"class":93},[87,12562,348],{"class":148},[87,12564,12565],{"class":93},"File",[87,12567,354],{"class":148},[87,12569,384],{"class":126},[87,12571,387],{"class":148},[87,12573,12574,12576,12579,12581,12584],{"class":89,"line":996},[87,12575,583],{"class":126},[87,12577,12578],{"class":148}," name ",[87,12580,1269],{"class":126},[87,12582,12583],{"class":97}," \"\u002F\"",[87,12585,595],{"class":148},[87,12587,12588,12591,12593],{"class":89,"line":1033},[87,12589,12590],{"class":148},"        name ",[87,12592,1671],{"class":126},[87,12594,12595],{"class":148}," f.prefix\n",[87,12597,12598,12601,12604],{"class":89,"line":1039},[87,12599,12600],{"class":148},"    } ",[87,12602,12603],{"class":126},"else",[87,12605,595],{"class":148},[87,12607,12608,12610,12612,12615,12617,12619,12622,12624,12626,12629,12631],{"class":89,"line":1044},[87,12609,12590],{"class":148},[87,12611,1671],{"class":126},[87,12613,12614],{"class":148}," f.prefix ",[87,12616,4955],{"class":126},[87,12618,12583],{"class":97},[87,12620,12621],{"class":126}," +",[87,12623,1331],{"class":148},[87,12625,8212],{"class":93},[87,12627,12628],{"class":148},"(name, ",[87,12630,8218],{"class":97},[87,12632,231],{"class":148},[87,12634,12635],{"class":89,"line":1059},[87,12636,620],{"class":148},[87,12638,12639,12641,12643,12646,12649,12651],{"class":89,"line":3261},[87,12640,399],{"class":126},[87,12642,11243],{"class":148},[87,12644,12645],{"class":93},"FS",[87,12647,12648],{"class":148},"(f.fs).",[87,12650,12550],{"class":93},[87,12652,12653],{"class":148},"(name)\n",[87,12655,12656],{"class":89,"line":3269},[87,12657,1062],{"class":148},[16,12659,12660],{},"两个细节：",[3705,12662,12663,12686],{},[51,12664,12665,12673,12674,8653,12676,12678,12679,12681,12682,12685],{},[38,12666,12667,12670,12671,12163],{},[30,12668,12669],{},"all:dist"," 里的 ",[30,12672,12162],{},"：embed 目录时默认会排除 ",[30,12675,8636],{},[30,12677,348],{}," 开头的文件（上篇坑二就是被这个坑了，当时靠改 Vite 输出名绕开）。",[30,12680,12162],{}," 前缀表示「全部包含」，前端构建产物里那些 ",[30,12683,12684],{},"_xxx.js"," 助手文件直接安全落地，不用再改前端配置；",[51,12687,12688,12691,12692,12694,12695,12698],{},[30,12689,12690],{},"http.FileSystem"," 包装：加了一层 ",[30,12693,8002],{}," 前缀映射，",[30,12696,12697],{},"http.FileServer"," 和 Gin 都能直接用。",[73,12700,12702],{"id":12701},"_4-路由静态服务-缓存策略","4. 路由：静态服务 + 缓存策略",[78,12704,12706],{"className":118,"code":12705,"language":94,"meta":83,"style":83},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\nfunc NewRouter(db *gorm.DB, uploadDir string) *gin.Engine {\n    r := gin.Default()\n    ...\n    dist := http.FileServer(web.Dist)\n    r.GET(\"\u002Fdist\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    r.GET(\"\u002Fdist\u002F*filepath\", func(c *gin.Context) {\n        p := c.Param(\"filepath\")\n        if p == \"\u002F\" || p == \"\u002Findex.html\" {\n            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n            c.Header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n        } else if strings.HasPrefix(p, \"\u002Fassets\u002F\") {\n            \u002F\u002F 带内容 hash 的资源可放心长缓存\n            c.Header(\"Cache-Control\", \"public, max-age=31536000, immutable\")\n        }\n        http.StripPrefix(\"\u002Fdist\", dist).ServeHTTP(c.Writer, c.Request)\n    })\n    r.GET(\"\u002F\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    ...\n}\n",[30,12707,12708,12713,12755,12767,12772,12787,12832,12861,12879,12900,12905,12924,12947,12952,12969,12973,12994,12998,13038,13042],{"__ignoreMap":83},[87,12709,12710],{"class":89,"line":90},[87,12711,12712],{"class":173},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\n",[87,12714,12715,12717,12720,12722,12725,12727,12730,12732,12735,12737,12740,12742,12744,12746,12748,12750,12753],{"class":89,"line":104},[87,12716,336],{"class":126},[87,12718,12719],{"class":93}," NewRouter",[87,12721,219],{"class":148},[87,12723,12724],{"class":341},"db",[87,12726,984],{"class":126},[87,12728,12729],{"class":93},"gorm",[87,12731,348],{"class":148},[87,12733,12734],{"class":93},"DB",[87,12736,354],{"class":148},[87,12738,12739],{"class":341},"uploadDir",[87,12741,11699],{"class":126},[87,12743,1208],{"class":148},[87,12745,371],{"class":126},[87,12747,987],{"class":93},[87,12749,348],{"class":148},[87,12751,12752],{"class":93},"Engine",[87,12754,595],{"class":148},[87,12756,12757,12759,12761,12763,12765],{"class":89,"line":145},[87,12758,794],{"class":148},[87,12760,152],{"class":126},[87,12762,799],{"class":148},[87,12764,802],{"class":93},[87,12766,805],{"class":148},[87,12768,12769],{"class":89,"line":164},[87,12770,12771],{"class":126},"    ...\n",[87,12773,12774,12777,12779,12781,12784],{"class":89,"line":177},[87,12775,12776],{"class":148},"    dist ",[87,12778,152],{"class":126},[87,12780,11243],{"class":148},[87,12782,12783],{"class":93},"FileServer",[87,12785,12786],{"class":148},"(web.Dist)\n",[87,12788,12789,12791,12793,12795,12798,12800,12802,12804,12806,12808,12810,12812,12814,12817,12820,12822,12825,12827,12829],{"class":89,"line":189},[87,12790,935],{"class":148},[87,12792,967],{"class":93},[87,12794,219],{"class":148},[87,12796,12797],{"class":97},"\"\u002Fdist\"",[87,12799,354],{"class":148},[87,12801,336],{"class":126},[87,12803,219],{"class":148},[87,12805,981],{"class":341},[87,12807,984],{"class":126},[87,12809,987],{"class":93},[87,12811,348],{"class":148},[87,12813,351],{"class":93},[87,12815,12816],{"class":148},") { c.",[87,12818,12819],{"class":93},"Redirect",[87,12821,219],{"class":148},[87,12823,12824],{"class":415},"302",[87,12826,354],{"class":148},[87,12828,12230],{"class":97},[87,12830,12831],{"class":148},") })\n",[87,12833,12834,12836,12838,12840,12843,12845,12847,12849,12851,12853,12855,12857,12859],{"class":89,"line":201},[87,12835,935],{"class":148},[87,12837,967],{"class":93},[87,12839,219],{"class":148},[87,12841,12842],{"class":97},"\"\u002Fdist\u002F*filepath\"",[87,12844,354],{"class":148},[87,12846,336],{"class":126},[87,12848,219],{"class":148},[87,12850,981],{"class":341},[87,12852,984],{"class":126},[87,12854,987],{"class":93},[87,12856,348],{"class":148},[87,12858,351],{"class":93},[87,12860,387],{"class":148},[87,12862,12863,12865,12867,12869,12872,12874,12877],{"class":89,"line":211},[87,12864,8205],{"class":148},[87,12866,152],{"class":126},[87,12868,1248],{"class":148},[87,12870,12871],{"class":93},"Param",[87,12873,219],{"class":148},[87,12875,12876],{"class":97},"\"filepath\"",[87,12878,231],{"class":148},[87,12880,12881,12883,12885,12887,12889,12891,12893,12895,12898],{"class":89,"line":228},[87,12882,1263],{"class":126},[87,12884,8227],{"class":148},[87,12886,1269],{"class":126},[87,12888,12583],{"class":97},[87,12890,1365],{"class":126},[87,12892,8227],{"class":148},[87,12894,1269],{"class":126},[87,12896,12897],{"class":97}," \"\u002Findex.html\"",[87,12899,595],{"class":148},[87,12901,12902],{"class":89,"line":390},[87,12903,12904],{"class":173},"            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n",[87,12906,12907,12909,12912,12914,12917,12919,12922],{"class":89,"line":396},[87,12908,1279],{"class":148},[87,12910,12911],{"class":93},"Header",[87,12913,219],{"class":148},[87,12915,12916],{"class":97},"\"Cache-Control\"",[87,12918,354],{"class":148},[87,12920,12921],{"class":97},"\"no-cache, no-store, must-revalidate\"",[87,12923,231],{"class":148},[87,12925,12926,12929,12931,12934,12936,12939,12942,12945],{"class":89,"line":419},[87,12927,12928],{"class":148},"        } ",[87,12930,12603],{"class":126},[87,12932,12933],{"class":126}," if",[87,12935,1331],{"class":148},[87,12937,12938],{"class":93},"HasPrefix",[87,12940,12941],{"class":148},"(p, ",[87,12943,12944],{"class":97},"\"\u002Fassets\u002F\"",[87,12946,387],{"class":148},[87,12948,12949],{"class":89,"line":617},[87,12950,12951],{"class":173},"            \u002F\u002F 带内容 hash 的资源可放心长缓存\n",[87,12953,12954,12956,12958,12960,12962,12964,12967],{"class":89,"line":623},[87,12955,1279],{"class":148},[87,12957,12911],{"class":93},[87,12959,219],{"class":148},[87,12961,12916],{"class":97},[87,12963,354],{"class":148},[87,12965,12966],{"class":97},"\"public, max-age=31536000, immutable\"",[87,12968,231],{"class":148},[87,12970,12971],{"class":89,"line":629},[87,12972,1317],{"class":148},[87,12974,12975,12978,12981,12983,12985,12988,12991],{"class":89,"line":647},[87,12976,12977],{"class":148},"        http.",[87,12979,12980],{"class":93},"StripPrefix",[87,12982,219],{"class":148},[87,12984,12797],{"class":97},[87,12986,12987],{"class":148},", dist).",[87,12989,12990],{"class":93},"ServeHTTP",[87,12992,12993],{"class":148},"(c.Writer, c.Request)\n",[87,12995,12996],{"class":89,"line":927},[87,12997,1036],{"class":148},[87,12999,13000,13002,13004,13006,13008,13010,13012,13014,13016,13018,13020,13022,13024,13026,13028,13030,13032,13034,13036],{"class":89,"line":932},[87,13001,935],{"class":148},[87,13003,967],{"class":93},[87,13005,219],{"class":148},[87,13007,8218],{"class":97},[87,13009,354],{"class":148},[87,13011,336],{"class":126},[87,13013,219],{"class":148},[87,13015,981],{"class":341},[87,13017,984],{"class":126},[87,13019,987],{"class":93},[87,13021,348],{"class":148},[87,13023,351],{"class":93},[87,13025,12816],{"class":148},[87,13027,12819],{"class":93},[87,13029,219],{"class":148},[87,13031,12824],{"class":415},[87,13033,354],{"class":148},[87,13035,12230],{"class":97},[87,13037,12831],{"class":148},[87,13039,13040],{"class":89,"line":951},[87,13041,12771],{"class":126},[87,13043,13044],{"class":89,"line":956},[87,13045,1062],{"class":148},[16,13047,13048],{},"缓存策略是内嵌发布的必修课（坑二细说）：入口禁缓存、hash 资源长缓存。",[73,13050,13052],{"id":13051},"_5-打包脚本windows-版-linux-交叉编译版","5. 打包脚本：Windows 版 + Linux 交叉编译版",[78,13054,13056],{"className":80,"code":13055,"language":82,"meta":83,"style":83},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\nset -euo pipefail\ncd \"$(dirname \"$0\")\"\n\nBINARY=\"${BINARY:-magic-img}\"\n\necho \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n(cd frontend && npm run build)\n\necho \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n(cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver)\nchmod +x \"backend\u002F$BINARY\"\n\necho \"[3\u002F3] 打包完成: backend\u002F$BINARY\"\n",[30,13057,13058,13063,13068,13078,13096,13100,13121,13125,13133,13152,13156,13163,13220,13235,13239],{"__ignoreMap":83},[87,13059,13060],{"class":89,"line":90},[87,13061,13062],{"class":173},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[87,13064,13065],{"class":89,"line":104},[87,13066,13067],{"class":173},"# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\n",[87,13069,13070,13072,13075],{"class":89,"line":145},[87,13071,8483],{"class":415},[87,13073,13074],{"class":415}," -euo",[87,13076,13077],{"class":97}," pipefail\n",[87,13079,13080,13082,13085,13088,13090,13093],{"class":89,"line":164},[87,13081,8562],{"class":415},[87,13083,13084],{"class":97}," \"$(",[87,13086,13087],{"class":93},"dirname",[87,13089,130],{"class":97},[87,13091,13092],{"class":415},"$0",[87,13094,13095],{"class":97},"\")\"\n",[87,13097,13098],{"class":89,"line":177},[87,13099,142],{"emptyLinePlaceholder":141},[87,13101,13102,13105,13107,13110,13112,13115,13118],{"class":89,"line":189},[87,13103,13104],{"class":148},"BINARY",[87,13106,1671],{"class":126},[87,13108,13109],{"class":97},"\"${",[87,13111,13104],{"class":148},[87,13113,13114],{"class":126},":-",[87,13116,13117],{"class":148},"magic-img",[87,13119,13120],{"class":97},"}\"\n",[87,13122,13123],{"class":89,"line":201},[87,13124,142],{"emptyLinePlaceholder":141},[87,13126,13127,13130],{"class":89,"line":211},[87,13128,13129],{"class":415},"echo",[87,13131,13132],{"class":97}," \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n",[87,13134,13135,13137,13139,13142,13144,13146,13148,13150],{"class":89,"line":228},[87,13136,219],{"class":148},[87,13138,8562],{"class":415},[87,13140,13141],{"class":97}," frontend",[87,13143,2246],{"class":148},[87,13145,7356],{"class":93},[87,13147,2197],{"class":97},[87,13149,2317],{"class":97},[87,13151,231],{"class":148},[87,13153,13154],{"class":89,"line":390},[87,13155,142],{"emptyLinePlaceholder":141},[87,13157,13158,13160],{"class":89,"line":396},[87,13159,13129],{"class":415},[87,13161,13162],{"class":97}," \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n",[87,13164,13165,13167,13169,13172,13175,13177,13179,13182,13184,13187,13190,13192,13195,13197,13199,13201,13203,13206,13209,13212,13215,13218],{"class":89,"line":419},[87,13166,219],{"class":148},[87,13168,8562],{"class":415},[87,13170,13171],{"class":97}," backend",[87,13173,13174],{"class":148}," && CGO_ENABLED",[87,13176,1671],{"class":126},[87,13178,1380],{"class":97},[87,13180,13181],{"class":148}," GOOS",[87,13183,1671],{"class":126},[87,13185,13186],{"class":97},"linux",[87,13188,13189],{"class":148}," GOARCH",[87,13191,1671],{"class":126},[87,13193,13194],{"class":97},"amd64",[87,13196,2656],{"class":93},[87,13198,2317],{"class":97},[87,13200,2326],{"class":415},[87,13202,130],{"class":97},[87,13204,13205],{"class":148},"$BINARY",[87,13207,13208],{"class":97},"\"",[87,13210,13211],{"class":415}," -ldflags=",[87,13213,13214],{"class":97},"\"-s -w\"",[87,13216,13217],{"class":97}," .\u002Fcmd\u002Fserver",[87,13219,231],{"class":148},[87,13221,13222,13225,13228,13231,13233],{"class":89,"line":617},[87,13223,13224],{"class":93},"chmod",[87,13226,13227],{"class":97}," +x",[87,13229,13230],{"class":97}," \"backend\u002F",[87,13232,13205],{"class":148},[87,13234,136],{"class":97},[87,13236,13237],{"class":89,"line":623},[87,13238,142],{"emptyLinePlaceholder":141},[87,13240,13241,13243,13246,13248],{"class":89,"line":629},[87,13242,13129],{"class":415},[87,13244,13245],{"class":97}," \"[3\u002F3] 打包完成: backend\u002F",[87,13247,13205],{"class":148},[87,13249,136],{"class":97},[16,13251,13252,13253,13256,13257,1178],{},"Windows 版（",[30,13254,13255],{},"build-win.bat","）同理，只是产物叫 ",[30,13258,13259],{},"magic-img.exe",[11,13261,8596],{"id":8596},[73,13263,13265,13266,13269,13270,13272],{"id":13264},"坑一goembed-dist-静默漏掉-_-开头文件升级解法","坑一：",[30,13267,13268],{},"\u002F\u002Fgo:embed dist"," 静默漏掉 ",[30,13271,8636],{}," 开头文件（升级解法）",[16,13274,13275,13276,13283,13284,13286],{},"上篇说过：embed 目录时",[38,13277,13278,13279,8653,13281,8637],{},"硬编码排除 ",[30,13280,8636],{},[30,13282,348],{},"。上篇的解法是改 Vite 输出文件名去掉 ",[30,13285,8636],{}," 前缀；这次直接用：",[78,13288,13290],{"className":118,"code":13289,"language":94,"meta":83,"style":83},"\u002F\u002Fgo:embed all:dist   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[30,13291,13292],{"__ignoreMap":83},[87,13293,13294,13297],{"class":89,"line":90},[87,13295,13296],{"class":173},"\u002F\u002Fgo:embed all:dist",[87,13298,13299],{"class":173},"   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[16,13301,13302],{},[38,13303,13304,13305,13307,13308,13310],{},"结论：能用 ",[30,13306,12162],{}," 就用 ",[30,13309,12162],{},"，从根上消灭这类\"文件莫名 404\"的问题。",[73,13312,13314],{"id":13313},"坑二开发版是新的打包版却是旧的","坑二：开发版是新的、打包版却是旧的",[16,13316,13317,13318,13321],{},"现象：Vite 开发服务一切正常，",[30,13319,13320],{},":8080\u002Fdist\u002F"," 打包版却是旧界面，强刷也不一定好。",[16,13323,13324,13325,13327,13328,13331,13332,13337],{},"排查过程值得记录：先是怀疑 ",[30,13326,8799],{}," 缓存了旧 embed（上篇坑三的思路），重编重启无效；然后直接抓取 8080 返回的 HTML 和 JS，发现",[38,13329,13330],{},"服务端内容已经是新的","——问题出在",[38,13333,13334,13335],{},"浏览器缓存了 ",[30,13336,12296],{},"：它引用旧 hash 的资源，一直不更新。",[16,13339,13340,13342,13343,13346,13347,13350],{},[38,13341,8614],{},"：入口文件响应 ",[30,13344,13345],{},"Cache-Control: no-store","（见上面路由代码），带 hash 的资源给 ",[30,13348,13349],{},"immutable"," 长缓存。升级后浏览器每次拉新入口，资源文件名一变就自动换新，两全其美。",[10949,13352,13353],{},[16,13354,13355],{},"教训：排查\"新旧不一致\"先分清是哪一层——服务端产物、浏览器入口缓存、还是资源缓存。抓包看响应内容再动手。",[73,13357,13359],{"id":13358},"坑三windows-上跑的-build-linuxsh产出的文件-linux-却执行不了","坑三：Windows 上跑的 build-linux.sh，产出的文件 Linux 却执行不了",[16,13361,13362,13363,7843,13366,13372,13373,13375],{},"最初脚本只写了 ",[30,13364,13365],{},"CGO_ENABLED=0 go build",[38,13367,13368,13369],{},"没指定 ",[30,13370,13371],{},"GOOS\u002FGOARCH","。在 Windows 上跑，产出的其实是个 Windows PE 文件（只是没叫 ",[30,13374,9101],{},"），传到 Linux 自然「无法执行」。",[16,13377,13378,13380],{},[38,13379,8614],{},"：脚本里显式交叉编译：",[78,13382,13384],{"className":80,"code":13383,"language":82,"meta":83,"style":83},"CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver\n",[30,13385,13386],{"__ignoreMap":83},[87,13387,13388,13391,13393,13395,13397,13399,13401,13403,13405,13407,13409,13411,13413,13415,13417,13419,13421,13423],{"class":89,"line":90},[87,13389,13390],{"class":148},"CGO_ENABLED",[87,13392,1671],{"class":126},[87,13394,1380],{"class":97},[87,13396,13181],{"class":148},[87,13398,1671],{"class":126},[87,13400,13186],{"class":97},[87,13402,13189],{"class":148},[87,13404,1671],{"class":126},[87,13406,13194],{"class":97},[87,13408,2656],{"class":93},[87,13410,2317],{"class":97},[87,13412,2326],{"class":415},[87,13414,130],{"class":97},[87,13416,13205],{"class":148},[87,13418,13208],{"class":97},[87,13420,13211],{"class":415},[87,13422,13214],{"class":97},[87,13424,13425],{"class":97}," .\u002Fcmd\u002Fserver\n",[16,13427,13428,13429,13432,13433,13436,13437],{},"能这么干的前提是",[38,13430,13431],{},"全依赖树无 cgo","——SQLite 用的是 modernc 纯 Go 驱动，bcrypt 用 ",[30,13434,13435],{},"x\u002Fcrypto","。这也是选型时就要留意的点：",[38,13438,13439],{},"想跨平台交叉编译，先把 cgo 依赖清干净。",[16,13441,13442,13443,13446,13447,13450],{},"另外两个小提醒：Windows 上编出的文件没有 Unix 执行位，上传服务器后记得 ",[30,13444,13445],{},"chmod +x","；",[30,13448,13449],{},"-ldflags=\"-s -w\""," 剥离符号，体积从 30MB+ 降到 26MB。",[73,13452,13454],{"id":13453},"坑四dist-被-gitignore-后embed-直接编译失败","坑四：dist 被 gitignore 后，embed 直接编译失败",[16,13456,13457,13459,13460,13462,13463,13466,13467,13469],{},[30,13458,12321],{}," 是构建产物、入了 .gitignore，但 ",[30,13461,7998],{}," 在",[38,13464,13465],{},"编译期","就要读它——刚 clone 下来直接 ",[30,13468,8799],{}," 会报 embed 找不到文件。",[16,13471,13472,13474,13475,13478],{},[38,13473,8614],{},"：gitignore 里保留 ",[30,13476,13477],{},"dist\u002F.gitkeep","（一个空文件），让目录存在于版本库中：",[78,13480,13484],{"className":13481,"code":13482,"language":13483,"meta":83,"style":83},"language-gitignore shiki shiki-themes github-light github-dark","backend\u002Fweb\u002Fdist\u002F*\n!backend\u002Fweb\u002Fdist\u002F\n!backend\u002Fweb\u002Fdist\u002F.gitkeep\n","gitignore",[30,13485,13486,13491,13496],{"__ignoreMap":83},[87,13487,13488],{"class":89,"line":90},[87,13489,13490],{},"backend\u002Fweb\u002Fdist\u002F*\n",[87,13492,13493],{"class":89,"line":104},[87,13494,13495],{},"!backend\u002Fweb\u002Fdist\u002F\n",[87,13497,13498],{"class":89,"line":145},[87,13499,13500],{},"!backend\u002Fweb\u002Fdist\u002F.gitkeep\n",[16,13502,13503,13504,13506,13507,13509],{},"这样 clone 后可以先 ",[30,13505,8799],{}," 通过，再 ",[30,13508,12314],{}," 灌入真实前端。",[73,13511,13513],{"id":13512},"坑五500kb-chunk-警告别慌","坑五：>500KB chunk 警告，别慌",[16,13515,13516,13517,13520,13521,1178],{},"Vite 构建会警告单 chunk 超过 500KB。这个项目里 Base UI 组件库全量引入，单 chunk 约 640KB——",[38,13518,13519],{},"属正常，不要为了消警告去拆配置","。真要优化就上路由级 ",[30,13522,13523],{},"React.lazy",[11,13525,8812],{"id":8812},[78,13527,13529],{"className":80,"code":13528,"language":82,"meta":83,"style":83},"$ build-win.bat        # backendmagic-img.exe\n$ bash build-linux.sh  # backendmagic-img（linux\u002Famd64 ELF）\n",[30,13530,13531,13541],{"__ignoreMap":83},[87,13532,13533,13535,13538],{"class":89,"line":90},[87,13534,4261],{"class":93},[87,13536,13537],{"class":97}," build-win.bat",[87,13539,13540],{"class":173},"        # backendmagic-img.exe\n",[87,13542,13543,13545,13547,13550],{"class":89,"line":104},[87,13544,4261],{"class":93},[87,13546,8824],{"class":97},[87,13548,13549],{"class":97}," build-linux.sh",[87,13551,13552],{"class":173},"  # backendmagic-img（linux\u002Famd64 ELF）\n",[16,13554,13555,13556,13559,13560,1178],{},"一个约 26MB 的二进制，内嵌 React 前端、Gin API、SQLite 数据初始化，拷到 Windows\u002FLinux 机器直接跑，浏览器访问 ",[30,13557,13558],{},"http:\u002F\u002Flocalhost:8080"," 自动重定向到 ",[30,13561,12300],{},[11,13563,2041],{"id":2041},[16,13565,13566],{},"这轮相比上篇的三点升级：",[48,13568,13569,13575,13585],{},[51,13570,13571,13574],{},[38,13572,13573],{},"hash 路由 + base \u002Fdist","：省掉 SPA fallback，静态服务即开即用；",[51,13576,13577,5279,13582,13584],{},[38,13578,13579,13581],{},[30,13580,12162],{}," embed 前缀",[30,13583,8636],{}," 开头文件问题一劳永逸；",[51,13586,13587,13593],{},[38,13588,13589,13590,13592],{},"显式 ",[30,13591,13371],{}," 交叉编译","：让「一个脚本、任意平台、产出 Linux 二进制」成为现实。",[16,13595,13596,13597,13600],{},"加上",[38,13598,13599],{},"入口 no-store \u002F 资源 immutable"," 的缓存策略，单文件分发的体验才算闭环——不然每次升级都要和浏览器缓存打架。",[2124,13602,13603],{},"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 .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 .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 .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 .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":83,"searchDepth":104,"depth":145,"links":13605},[13606,13607,13608,13615,13623,13624],{"id":4021,"depth":104,"text":4021},{"id":7992,"depth":104,"text":7992},{"id":8006,"depth":104,"text":8006,"children":13609},[13610,13611,13612,13613,13614],{"id":12190,"depth":145,"text":12191},{"id":12308,"depth":145,"text":12309},{"id":12364,"depth":145,"text":12365},{"id":12701,"depth":145,"text":12702},{"id":13051,"depth":145,"text":13052},{"id":8596,"depth":104,"text":8596,"children":13616},[13617,13619,13620,13621,13622],{"id":13264,"depth":145,"text":13618},"坑一：\u002F\u002Fgo:embed dist 静默漏掉 _ 开头文件（升级解法）",{"id":13313,"depth":145,"text":13314},{"id":13358,"depth":145,"text":13359},{"id":13453,"depth":145,"text":13454},{"id":13512,"depth":145,"text":13513},{"id":8812,"depth":104,"text":8812},{"id":2041,"depth":104,"text":2041},"以 Magic Img（React 19 + Vite + Gin + SQLite）为实例，记录把前端塞进 Go 二进制、并在 Windows 上交叉编译 Linux 产物的完整过程：embed 的 all 前缀、hash 路由、缓存策略、GOOS 交叉编译等实操坑。","\u002Fposts\u002Fgo-embed-react-single-binary\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-embed-react-single-binary","2026-08-16T00:00:00",{"title":12102,"description":13625},"posts\u002Fgo-embed-react-single-binary\u002Findex",[2158,10918,8046,13633,13634],"交叉编译","打包","WV-T-MOO5Cu-aycw-Ony8s9hjA3_k0S4XU-T0QsCow0",{"id":13637,"title":13638,"author":6,"body":13639,"description":14884,"difficulty":3367,"draft":2148,"extension":2149,"image":14885,"meta":14886,"navigation":141,"path":14887,"pinned":2148,"published":13629,"seo":14888,"series":14889,"seriesOrder":90,"stem":14890,"tags":14891,"updated":12098,"__hash__":14895},"posts\u002Fposts\u002Freact-photo-album-masonry\u002Findex.md","用 react-photo-album 给生图图库做瀑布流排版",{"type":8,"value":13640,"toc":14867},[13641,13644,13647,13665,13672,13736,13747,13750,13773,13776,13780,13794,13798,13814,13992,13996,14393,14396,14430,14433,14437,14447,14453,14478,14483,14494,14500,14511,14514,14554,14567,14690,14702,14706,14713,14731,14736,14826,14832,14836,14847,14850,14853,14859,14864],[11,13642,13643],{"id":13643},"技术背景",[16,13645,13646],{},"Magic Img 是我做的一个 AI 生图提示词管理工具（自托管、本地优先），其中「生图图库」页面要把所有生成图片以图片墙的形式展示出来。图片墙有两个天然矛盾：",[48,13648,13649,13655],{},[51,13650,13651,13654],{},[38,13652,13653],{},"等宽网格","（CSS Grid）：整齐，但每行高度由最高的那张决定——不同比例的图片混排时，矮图下面会留下大洞，看起来\"很乱\"；",[51,13656,13657,13660,13661,13664],{},[38,13658,13659],{},"原生比例展示","：图库必须不裁剪（这是产品约束），所以不能用 ",[30,13662,13663],{},"object-cover"," 强行统一比例。",[16,13666,13667,13668,13671],{},"业内的标准解法是 ",[38,13669,13670],{},"Masonry 瀑布流","：按列紧密堆放，每张图保持原始比例，列间高度错落但整体紧凑。实现瀑布流的常见方案有三类：",[2043,13673,13674,13688],{},[2046,13675,13676],{},[2049,13677,13678,13680,13683,13686],{},[2052,13679,4816],{},[2052,13681,13682],{},"依赖",[2052,13684,13685],{},"顺序",[2052,13687,2057],{},[2059,13689,13690,13707,13720],{},[2049,13691,13692,13699,13701,13704],{},[2064,13693,13694,13695,13698],{},"CSS ",[30,13696,13697],{},"columns"," 多列",[2064,13700,3540],{},[2064,13702,13703],{},"纵向阅读序（先下后右）",[2064,13705,13706],{},"最新图片不再出现在左上角，时间语义丢失",[2049,13708,13709,13712,13714,13717],{},[2064,13710,13711],{},"grid row-span 自研",[2064,13713,3540],{},[2064,13715,13716],{},"保持",[2064,13718,13719],{},"需固定列数 + 固定行高，逻辑要自己写",[2049,13721,13722,13727,13730,13733],{},[2064,13723,13724],{},[38,13725,13726],{},"react-photo-album",[2064,13728,13729],{},"~4KB，无依赖",[2064,13731,13732],{},"基本保持（逐列最短列填充）",[2064,13734,13735],{},"专为相册墙设计，TS 友好，维护活跃",[16,13737,13738,13739,13742,13743,13746],{},"最终选了 ",[38,13740,13741],{},"react-photo-album v3","：体积小、类型齐全，而且它的 Masonry 模式恰好需要「每张图的 ",[30,13744,13745],{},"width\u002Fheight","」——我们的数据库里本来就存了，零额外成本。",[11,13748,13749],{"id":13749},"需求梳理",[48,13751,13752,13758,13764,13767,13770],{},[51,13753,13754,13755,13446],{},"展示全部生成图（21 张起，会持续增长），",[38,13756,13757],{},"原比例、不裁剪",[51,13759,13760,13761,13446],{},"不同尺寸（2304×1728、4608×3456 等）混排时",[38,13762,13763],{},"紧凑、不留洞",[51,13765,13766],{},"顺序基本保持「新图靠前」；",[51,13768,13769],{},"点击任意一张打开大图预览（YARL 灯箱），悬停显示所属提示词；",[51,13771,13772],{},"平板性能：墙上只用 300px 缩略图，原图只进灯箱按需加载。",[11,13774,13775],{"id":13775},"安装与使用",[73,13777,13779],{"id":13778},"_1-安装","1. 安装",[78,13781,13783],{"className":80,"code":13782,"language":82,"meta":83,"style":83},"npm install react-photo-album\n",[30,13784,13785],{"__ignoreMap":83},[87,13786,13787,13789,13791],{"class":89,"line":90},[87,13788,7356],{"class":93},[87,13790,2251],{"class":97},[87,13792,13793],{"class":97}," react-photo-album\n",[73,13795,13797],{"id":13796},"_2-准备数据关键输入是宽高","2. 准备数据（关键输入是宽高）",[16,13799,13800,13801,13804,13805,8619,13807,8619,13810,13813],{},"库要求的 ",[30,13802,13803],{},"Photo"," 对象只有三个必需字段：",[30,13806,4678],{},[30,13808,13809],{},"width",[30,13811,13812],{},"height","。我们的接口本来就返回每张图的原始宽高：",[78,13815,13817],{"className":12248,"code":13816,"language":12250,"meta":83,"style":83},"import PhotoAlbum, { type Photo } from \"react-photo-album\"\n\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\nimport \"react-photo-album\u002Fstyles.css\"\n\ninterface GalleryPhoto extends Photo {\n  prompt_text?: string\n}\n\nconst albumPhotos: GalleryPhoto[] = images.map((img) => ({\n  key: String(img.id),\n  src: assetUrl(img.thumb_path) || assetUrl(img.file_path),\n  width: img.width,\n  height: img.height,\n  alt: img.prompt_text || `生成图 ${img.id}`,\n  prompt_text: img.prompt_text,\n}))\n",[30,13818,13819,13837,13842,13849,13853,13869,13879,13883,13887,13920,13931,13950,13955,13960,13982,13987],{"__ignoreMap":83},[87,13820,13821,13823,13826,13828,13831,13834],{"class":89,"line":90},[87,13822,127],{"class":126},[87,13824,13825],{"class":148}," PhotoAlbum, { ",[87,13827,1564],{"class":126},[87,13829,13830],{"class":148}," Photo } ",[87,13832,13833],{"class":126},"from",[87,13835,13836],{"class":97}," \"react-photo-album\"\n",[87,13838,13839],{"class":89,"line":104},[87,13840,13841],{"class":173},"\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\n",[87,13843,13844,13846],{"class":89,"line":145},[87,13845,127],{"class":126},[87,13847,13848],{"class":97}," \"react-photo-album\u002Fstyles.css\"\n",[87,13850,13851],{"class":89,"line":164},[87,13852,142],{"emptyLinePlaceholder":141},[87,13854,13855,13858,13861,13864,13867],{"class":89,"line":177},[87,13856,13857],{"class":126},"interface",[87,13859,13860],{"class":93}," GalleryPhoto",[87,13862,13863],{"class":126}," extends",[87,13865,13866],{"class":93}," Photo",[87,13868,595],{"class":148},[87,13870,13871,13874,13877],{"class":89,"line":189},[87,13872,13873],{"class":341},"  prompt_text",[87,13875,13876],{"class":126},"?:",[87,13878,1570],{"class":415},[87,13880,13881],{"class":89,"line":201},[87,13882,1062],{"class":148},[87,13884,13885],{"class":89,"line":211},[87,13886,142],{"emptyLinePlaceholder":141},[87,13888,13889,13891,13894,13896,13898,13900,13902,13905,13908,13911,13913,13915,13917],{"class":89,"line":228},[87,13890,1575],{"class":126},[87,13892,13893],{"class":415}," albumPhotos",[87,13895,3086],{"class":126},[87,13897,13860],{"class":93},[87,13899,4170],{"class":148},[87,13901,1671],{"class":126},[87,13903,13904],{"class":148}," images.",[87,13906,13907],{"class":93},"map",[87,13909,13910],{"class":148},"((",[87,13912,2426],{"class":341},[87,13914,1208],{"class":148},[87,13916,4567],{"class":126},[87,13918,13919],{"class":148}," ({\n",[87,13921,13922,13925,13928],{"class":89,"line":390},[87,13923,13924],{"class":148},"  key: ",[87,13926,13927],{"class":93},"String",[87,13929,13930],{"class":148},"(img.id),\n",[87,13932,13933,13936,13939,13942,13944,13947],{"class":89,"line":396},[87,13934,13935],{"class":148},"  src: ",[87,13937,13938],{"class":93},"assetUrl",[87,13940,13941],{"class":148},"(img.thumb_path) ",[87,13943,4355],{"class":126},[87,13945,13946],{"class":93}," assetUrl",[87,13948,13949],{"class":148},"(img.file_path),\n",[87,13951,13952],{"class":89,"line":419},[87,13953,13954],{"class":148},"  width: img.width,\n",[87,13956,13957],{"class":89,"line":617},[87,13958,13959],{"class":148},"  height: img.height,\n",[87,13961,13962,13965,13967,13970,13972,13974,13977,13980],{"class":89,"line":623},[87,13963,13964],{"class":148},"  alt: img.prompt_text ",[87,13966,4355],{"class":126},[87,13968,13969],{"class":97}," `生成图 ${",[87,13971,2426],{"class":148},[87,13973,348],{"class":97},[87,13975,13976],{"class":148},"id",[87,13978,13979],{"class":97},"}`",[87,13981,294],{"class":148},[87,13983,13984],{"class":89,"line":629},[87,13985,13986],{"class":148},"  prompt_text: img.prompt_text,\n",[87,13988,13989],{"class":89,"line":647},[87,13990,13991],{"class":148},"}))\n",[73,13993,13995],{"id":13994},"_3-使用-masonry-布局","3. 使用 Masonry 布局",[78,13997,13999],{"className":12248,"code":13998,"language":12250,"meta":83,"style":83},"\u003CPhotoAlbum\n  layout=\"masonry\"\n  columns={(cw) => (cw \u003C 640 ? 2 : cw \u003C 960 ? 3 : cw \u003C 1280 ? 4 : 5)}\n  spacing={16}\n  padding={0}\n  photos={albumPhotos}\n  onClick={({ index }) => openPreview(index)}\n  render={{\n    photo: (props, context) => {\n      const { onClick } = props\n      const { photo, index, width, height } = context\n      return (\n        \u003Cbutton type=\"button\" onClick={onClick} className=\"group relative block w-full ...\">\n          \u003Cimg\n            src={photo.src}\n            alt={photo.alt || \"\"}\n            decoding=\"async\"\n            style={{ width, height }}\n          \u002F>\n          {\u002F* hover 提示词浮层 *\u002F}\n          \u003Cdiv className=\"... opacity-0 group-hover:opacity-100\">\n            {photo.prompt_text}\n          \u003C\u002Fdiv>\n        \u003C\u002Fbutton>\n      )\n    },\n  }}\n\u002F>\n",[30,14000,14001,14008,14018,14086,14100,14113,14123,14147,14157,14178,14196,14224,14231,14265,14273,14283,14299,14309,14319,14324,14334,14351,14356,14365,14374,14379,14383,14388],{"__ignoreMap":83},[87,14002,14003,14005],{"class":89,"line":90},[87,14004,3089],{"class":148},[87,14006,14007],{"class":415},"PhotoAlbum\n",[87,14009,14010,14013,14015],{"class":89,"line":104},[87,14011,14012],{"class":93},"  layout",[87,14014,1671],{"class":126},[87,14016,14017],{"class":97},"\"masonry\"\n",[87,14019,14020,14023,14025,14028,14031,14033,14035,14038,14040,14043,14046,14048,14051,14054,14056,14059,14061,14064,14066,14068,14070,14073,14075,14078,14080,14083],{"class":89,"line":145},[87,14021,14022],{"class":93},"  columns",[87,14024,1671],{"class":126},[87,14026,14027],{"class":148},"{(",[87,14029,14030],{"class":341},"cw",[87,14032,1208],{"class":148},[87,14034,4567],{"class":126},[87,14036,14037],{"class":148}," (cw ",[87,14039,3089],{"class":126},[87,14041,14042],{"class":415}," 640",[87,14044,14045],{"class":126}," ?",[87,14047,1362],{"class":415},[87,14049,14050],{"class":126}," :",[87,14052,14053],{"class":148}," cw ",[87,14055,3089],{"class":126},[87,14057,14058],{"class":415}," 960",[87,14060,14045],{"class":126},[87,14062,14063],{"class":415}," 3",[87,14065,14050],{"class":126},[87,14067,14053],{"class":148},[87,14069,3089],{"class":126},[87,14071,14072],{"class":415}," 1280",[87,14074,14045],{"class":126},[87,14076,14077],{"class":415}," 4",[87,14079,14050],{"class":126},[87,14081,14082],{"class":415}," 5",[87,14084,14085],{"class":148},")}\n",[87,14087,14088,14091,14093,14095,14098],{"class":89,"line":164},[87,14089,14090],{"class":93},"  spacing",[87,14092,1671],{"class":126},[87,14094,1019],{"class":148},[87,14096,14097],{"class":415},"16",[87,14099,1062],{"class":148},[87,14101,14102,14105,14107,14109,14111],{"class":89,"line":177},[87,14103,14104],{"class":93},"  padding",[87,14106,1671],{"class":126},[87,14108,1019],{"class":148},[87,14110,1380],{"class":415},[87,14112,1062],{"class":148},[87,14114,14115,14118,14120],{"class":89,"line":189},[87,14116,14117],{"class":93},"  photos",[87,14119,1671],{"class":126},[87,14121,14122],{"class":148},"{albumPhotos}\n",[87,14124,14125,14128,14130,14133,14136,14139,14141,14144],{"class":89,"line":201},[87,14126,14127],{"class":93},"  onClick",[87,14129,1671],{"class":126},[87,14131,14132],{"class":148},"{({ ",[87,14134,14135],{"class":341},"index",[87,14137,14138],{"class":148}," }) ",[87,14140,4567],{"class":126},[87,14142,14143],{"class":93}," openPreview",[87,14145,14146],{"class":148},"(index)}\n",[87,14148,14149,14152,14154],{"class":89,"line":211},[87,14150,14151],{"class":93},"  render",[87,14153,1671],{"class":126},[87,14155,14156],{"class":148},"{{\n",[87,14158,14159,14162,14165,14167,14169,14172,14174,14176],{"class":89,"line":228},[87,14160,14161],{"class":93},"    photo",[87,14163,14164],{"class":148},": (",[87,14166,4673],{"class":341},[87,14168,354],{"class":148},[87,14170,14171],{"class":341},"context",[87,14173,1208],{"class":148},[87,14175,4567],{"class":126},[87,14177,595],{"class":148},[87,14179,14180,14182,14185,14188,14191,14193],{"class":89,"line":390},[87,14181,4282],{"class":126},[87,14183,14184],{"class":148}," { ",[87,14186,14187],{"class":415},"onClick",[87,14189,14190],{"class":148}," } ",[87,14192,1671],{"class":126},[87,14194,14195],{"class":148}," props\n",[87,14197,14198,14200,14202,14205,14207,14209,14211,14213,14215,14217,14219,14221],{"class":89,"line":396},[87,14199,4282],{"class":126},[87,14201,14184],{"class":148},[87,14203,14204],{"class":415},"photo",[87,14206,354],{"class":148},[87,14208,14135],{"class":415},[87,14210,354],{"class":148},[87,14212,13809],{"class":415},[87,14214,354],{"class":148},[87,14216,13812],{"class":415},[87,14218,14190],{"class":148},[87,14220,1671],{"class":126},[87,14222,14223],{"class":148}," context\n",[87,14225,14226,14229],{"class":89,"line":419},[87,14227,14228],{"class":126},"      return",[87,14230,1578],{"class":148},[87,14232,14233,14236,14239,14242,14244,14247,14250,14252,14255,14258,14260,14263],{"class":89,"line":617},[87,14234,14235],{"class":148},"        \u003C",[87,14237,14238],{"class":3855},"button",[87,14240,14241],{"class":93}," type",[87,14243,1671],{"class":126},[87,14245,14246],{"class":97},"\"button\"",[87,14248,14249],{"class":93}," onClick",[87,14251,1671],{"class":126},[87,14253,14254],{"class":148},"{onClick} ",[87,14256,14257],{"class":93},"className",[87,14259,1671],{"class":126},[87,14261,14262],{"class":97},"\"group relative block w-full ...\"",[87,14264,2749],{"class":148},[87,14266,14267,14270],{"class":89,"line":623},[87,14268,14269],{"class":148},"          \u003C",[87,14271,14272],{"class":3855},"img\n",[87,14274,14275,14278,14280],{"class":89,"line":629},[87,14276,14277],{"class":93},"            src",[87,14279,1671],{"class":126},[87,14281,14282],{"class":148},"{photo.src}\n",[87,14284,14285,14288,14290,14293,14295,14297],{"class":89,"line":647},[87,14286,14287],{"class":93},"            alt",[87,14289,1671],{"class":126},[87,14291,14292],{"class":148},"{photo.alt ",[87,14294,4355],{"class":126},[87,14296,1272],{"class":97},[87,14298,1062],{"class":148},[87,14300,14301,14304,14306],{"class":89,"line":927},[87,14302,14303],{"class":93},"            decoding",[87,14305,1671],{"class":126},[87,14307,14308],{"class":97},"\"async\"\n",[87,14310,14311,14314,14316],{"class":89,"line":932},[87,14312,14313],{"class":93},"            style",[87,14315,1671],{"class":126},[87,14317,14318],{"class":148},"{{ width, height }}\n",[87,14320,14321],{"class":89,"line":951},[87,14322,14323],{"class":148},"          \u002F>\n",[87,14325,14326,14329,14332],{"class":89,"line":956},[87,14327,14328],{"class":148},"          {",[87,14330,14331],{"class":173},"\u002F* hover 提示词浮层 *\u002F",[87,14333,1062],{"class":148},[87,14335,14336,14338,14341,14344,14346,14349],{"class":89,"line":962},[87,14337,14269],{"class":148},[87,14339,14340],{"class":3855},"div",[87,14342,14343],{"class":93}," className",[87,14345,1671],{"class":126},[87,14347,14348],{"class":97},"\"... opacity-0 group-hover:opacity-100\"",[87,14350,2749],{"class":148},[87,14352,14353],{"class":89,"line":996},[87,14354,14355],{"class":148},"            {photo.prompt_text}\n",[87,14357,14358,14361,14363],{"class":89,"line":1033},[87,14359,14360],{"class":148},"          \u003C\u002F",[87,14362,14340],{"class":3855},[87,14364,2749],{"class":148},[87,14366,14367,14370,14372],{"class":89,"line":1039},[87,14368,14369],{"class":148},"        \u003C\u002F",[87,14371,14238],{"class":3855},[87,14373,2749],{"class":148},[87,14375,14376],{"class":89,"line":1044},[87,14377,14378],{"class":148},"      )\n",[87,14380,14381],{"class":89,"line":1059},[87,14382,8773],{"class":148},[87,14384,14385],{"class":89,"line":3261},[87,14386,14387],{"class":148},"  }}\n",[87,14389,14390],{"class":89,"line":3269},[87,14391,14392],{"class":148},"\u002F>\n",[16,14394,14395],{},"几个要点：",[3705,14397,14398,14407,14420],{},[51,14399,14400,14402,14403,14406],{},[30,14401,13697],{}," 可以是数字或 ",[30,14404,14405],{},"(containerWidth) => number"," 函数，做响应式列数很方便；",[51,14408,14409,14410,14412,14413,14416,14417,14419],{},"点击回调 ",[30,14411,14187],{}," 里拿到的是",[38,14414,14415],{},"原始数组下标"," ",[30,14418,14135],{},"，直接传给灯箱即可；",[51,14421,14422,14423,14426,14427,14429],{},"自定义 ",[30,14424,14425],{},"render.photo"," 后，图片尺寸要自己用 context 里的 ",[30,14428,13745],{},"（渲染尺寸）设置——这两个值库已经按列宽算好了。",[11,14431,14432],{"id":14432},"遇到的坑",[73,14434,14436],{"id":14435},"坑-1忘记引入样式表-一张一列","坑 1：忘记引入样式表 → 「一张一列」",[16,14438,14439,14440,14443,14444,1178],{},"这是最坑的一个。装完包、写好 ",[30,14441,14442],{},"\u003CPhotoAlbum layout=\"masonry\">","，页面渲染出来却是：",[38,14445,14446],{},"每张图占一整行，右侧大片置灰空白",[16,14448,14449,14450,14452],{},"排查过程：先怀疑是 ",[30,14451,13697],{}," 函数没生效，改成固定数字无效；再检查 DOM——容器和 5 个列（track）都在，列的宽度也算对了，但图片全都堆在布局里。",[16,14454,14455,14456,14459,14460,8619,14463,14466,14467,14473,14474,14477],{},"最后翻包的 ",[30,14457,14458],{},"package.json"," 发现它导出了独立的 CSS 文件（",[30,14461,14462],{},".\u002Fstyles.css",[30,14464,14465],{},".\u002Fmasonry.css"," 等），而",[38,14468,14469,14470],{},"官方 README 示例都带了一行 ",[30,14471,14472],{},"import \"react-photo-album\u002Fstyles.css\"","。容器是 flex、列是 flex-column、列宽按 ",[30,14475,14476],{},"--react-photo-album--columns"," 计算——这些规则全在这个样式表里，不引入就退化成普通块级堆叠。",[16,14479,14480,14482],{},[38,14481,8614],{},"：顶部加一行：",[78,14484,14486],{"className":12248,"code":14485,"language":12250,"meta":83,"style":83},"import \"react-photo-album\u002Fstyles.css\"\n",[30,14487,14488],{"__ignoreMap":83},[87,14489,14490,14492],{"class":89,"line":90},[87,14491,127],{"class":126},[87,14493,13848],{"class":97},[16,14495,14496,14497],{},"经验：",[38,14498,14499],{},"用这类\"无运行时依赖\"的布局库，先看它的 exports 里有没有独立 CSS，有就一定要引。",[73,14501,14503,14504,14506,14507,14510],{"id":14502},"坑-2renderphoto-是-props-context-双参签名","坑 2：",[30,14505,14425],{}," 是 ",[30,14508,14509],{},"(props, context)"," 双参签名",[16,14512,14513],{},"自定义渲染时我凭直觉写了：",[78,14515,14517],{"className":12248,"code":14516,"language":12250,"meta":83,"style":83},"photo: ({ photo, index, width, height, onClick }) => ... \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[30,14518,14519],{"__ignoreMap":83},[87,14520,14521,14523,14526,14528,14530,14532,14534,14536,14538,14540,14542,14544,14546,14548,14551],{"class":89,"line":90},[87,14522,14204],{"class":93},[87,14524,14525],{"class":148},": ({ ",[87,14527,14204],{"class":341},[87,14529,354],{"class":148},[87,14531,14135],{"class":341},[87,14533,354],{"class":148},[87,14535,13809],{"class":341},[87,14537,354],{"class":148},[87,14539,13812],{"class":341},[87,14541,354],{"class":148},[87,14543,14187],{"class":341},[87,14545,14138],{"class":148},[87,14547,4567],{"class":126},[87,14549,14550],{"class":126}," ...",[87,14552,14553],{"class":173}," \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[16,14555,14556,14557,14559,14560,14563,14564,5279],{},"TS 报错提示 ",[30,14558,14204],{}," 等属性不存在于 ",[30,14561,14562],{},"RenderPhotoProps","。看类型定义才发现 v3 的渲染函数是",[38,14565,14566],{},"两个参数",[78,14568,14570],{"className":12248,"code":14569,"language":12250,"meta":83,"style":83},"type RenderFunction\u003CProps, Context> = (props: Props, context: Context) => ReactNode\n\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\nphoto: (props, context) => {\n  const { onClick } = props\n  const { photo, index, width, height } = context\n  ...\n}\n",[30,14571,14572,14618,14623,14643,14657,14682,14686],{"__ignoreMap":83},[87,14573,14574,14576,14579,14581,14584,14586,14588,14591,14593,14595,14597,14599,14602,14604,14606,14608,14611,14613,14615],{"class":89,"line":90},[87,14575,1564],{"class":126},[87,14577,14578],{"class":93}," RenderFunction",[87,14580,3089],{"class":148},[87,14582,14583],{"class":93},"Props",[87,14585,354],{"class":148},[87,14587,351],{"class":93},[87,14589,14590],{"class":148},"> ",[87,14592,1671],{"class":126},[87,14594,1817],{"class":148},[87,14596,4673],{"class":341},[87,14598,3086],{"class":126},[87,14600,14601],{"class":93}," Props",[87,14603,354],{"class":148},[87,14605,14171],{"class":341},[87,14607,3086],{"class":126},[87,14609,14610],{"class":93}," Context",[87,14612,1208],{"class":148},[87,14614,4567],{"class":126},[87,14616,14617],{"class":93}," ReactNode\n",[87,14619,14620],{"class":89,"line":104},[87,14621,14622],{"class":173},"\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\n",[87,14624,14625,14627,14629,14631,14633,14635,14637,14639,14641],{"class":89,"line":145},[87,14626,14204],{"class":93},[87,14628,3086],{"class":126},[87,14630,1817],{"class":148},[87,14632,4673],{"class":341},[87,14634,354],{"class":148},[87,14636,14171],{"class":341},[87,14638,1208],{"class":148},[87,14640,4567],{"class":126},[87,14642,595],{"class":148},[87,14644,14645,14647,14649,14651,14654],{"class":89,"line":164},[87,14646,4627],{"class":93},[87,14648,14184],{"class":148},[87,14650,14187],{"class":341},[87,14652,14653],{"class":148}," } = ",[87,14655,14656],{"class":341},"props\n",[87,14658,14659,14661,14663,14665,14667,14669,14671,14673,14675,14677,14679],{"class":89,"line":177},[87,14660,4627],{"class":93},[87,14662,14184],{"class":148},[87,14664,14204],{"class":341},[87,14666,354],{"class":148},[87,14668,14135],{"class":341},[87,14670,354],{"class":148},[87,14672,13809],{"class":341},[87,14674,354],{"class":148},[87,14676,13812],{"class":341},[87,14678,14653],{"class":148},[87,14680,14681],{"class":341},"context\n",[87,14683,14684],{"class":89,"line":189},[87,14685,12241],{"class":126},[87,14687,14688],{"class":89,"line":201},[87,14689,1062],{"class":148},[16,14691,14692,14694,14695,14697,14698,14701],{},[38,14693,8614],{},"：按 ",[30,14696,14509],{}," 拆解即可。装新库先扫一眼 ",[30,14699,14700],{},".d.ts"," 里的 Render 类型签名，能少走很多弯路。",[73,14703,14705],{"id":14704},"坑-3测试断言把dom-顺序当成了视觉顺序","坑 3：测试断言把「DOM 顺序」当成了「视觉顺序」",[16,14707,14708,14709,14712],{},"布局修好后，我在浏览器自测脚本里加了一条断言：取前两个 ",[30,14710,14711],{},"\u003Cimg>","，比较它们的左坐标，认为应该并排（不同列）。",[16,14714,14715,14716,14719,14720,8653,14723,14726,14727,14730],{},"结果断言失败——但页面明明是 5 列并排。排查发现：",[38,14717,14718],{},"瀑布流的 DOM 是按\"列\"组织的","（第 1 列的全部图片在前，第 2 列的在后），所以 ",[30,14721,14722],{},"img[0]",[30,14724,14725],{},"img[1]"," 属于",[38,14728,14729],{},"同一列","，左坐标当然相同。",[16,14732,14733,14735],{},[38,14734,8614],{},"：断言改为比较「第 1 列的首张图 vs 第 2 列的首张图」：",[78,14737,14741],{"className":14738,"code":14739,"language":14740,"meta":83,"style":83},"language-js shiki shiki-themes github-light github-dark","const a = tracks[0].querySelector(\"img\")\nconst b = tracks[1].querySelector(\"img\")\nreturn Math.abs(a.getBoundingClientRect().left - b.getBoundingClientRect().left) > 10\n","js",[30,14742,14743,14768,14791],{"__ignoreMap":83},[87,14744,14745,14747,14749,14751,14754,14756,14758,14761,14763,14766],{"class":89,"line":90},[87,14746,1575],{"class":126},[87,14748,5385],{"class":415},[87,14750,1589],{"class":126},[87,14752,14753],{"class":148}," tracks[",[87,14755,1380],{"class":415},[87,14757,4297],{"class":148},[87,14759,14760],{"class":93},"querySelector",[87,14762,219],{"class":148},[87,14764,14765],{"class":97},"\"img\"",[87,14767,231],{"class":148},[87,14769,14770,14772,14775,14777,14779,14781,14783,14785,14787,14789],{"class":89,"line":104},[87,14771,1575],{"class":126},[87,14773,14774],{"class":415}," b",[87,14776,1589],{"class":126},[87,14778,14753],{"class":148},[87,14780,1447],{"class":415},[87,14782,4297],{"class":148},[87,14784,14760],{"class":93},[87,14786,219],{"class":148},[87,14788,14765],{"class":97},[87,14790,231],{"class":148},[87,14792,14793,14795,14798,14801,14804,14807,14810,14813,14816,14818,14821,14823],{"class":89,"line":145},[87,14794,4198],{"class":126},[87,14796,14797],{"class":148}," Math.",[87,14799,14800],{"class":93},"abs",[87,14802,14803],{"class":148},"(a.",[87,14805,14806],{"class":93},"getBoundingClientRect",[87,14808,14809],{"class":148},"().left ",[87,14811,14812],{"class":126},"-",[87,14814,14815],{"class":148}," b.",[87,14817,14806],{"class":93},[87,14819,14820],{"class":148},"().left) ",[87,14822,2702],{"class":126},[87,14824,14825],{"class":415}," 10\n",[16,14827,14496,14828,14831],{},[38,14829,14830],{},"验证布局类库时，先搞清楚它生成的 DOM 结构，再写断言","；另外把「真实浏览器探针」当排查手段（直接打印各 track 的 x 坐标和子元素数量），比盯着代码猜快得多。",[73,14833,14835],{"id":14834},"坑-4等宽网格的洞引入前就存在的产品问题","坑 4：等宽网格的\"洞\"（引入前就存在的产品问题）",[16,14837,14838,14839,14842,14843,14846],{},"顺带记录一下为什么换掉原来的布局：CSS Grid ",[30,14840,14841],{},"repeat(auto-fill, minmax(220px, 1fr))"," 等宽铺满，图片 ",[30,14844,14845],{},"w-full h-auto","。当一行里混有不同比例时，行高取最高的那张，矮图底部留空，几行错落下来就显得乱；另外窗口宽度\"差一点放不下下一张\"时，右侧会空出近一整列。",[16,14848,14849],{},"换成 Masonry 后这两个问题都消失了——列内紧密堆叠、列数自适应。",[11,14851,14852],{"id":14852},"效果与小结",[16,14854,14855,14856,14858],{},"最终效果：21 张不同比例的生成图在 5 列瀑布流里紧密排列，原比例不裁剪，悬停出提示词浮层，点击进 YARL 大图预览；缩略图总大小仅 360KB，进页面即全量加载 + ",[30,14857,13349],{}," 长缓存，平板上快速滚动也跟手。",[16,14860,12027,14861],{},[38,14862,14863],{},"react-photo-album 是个小但专业的相册布局库，只要记住三件事——引样式表、喂宽高、render 函数是双参——就能在十分钟内把图片墙升级成真正的瀑布流。",[2124,14865,14866],{},"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}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":83,"searchDepth":104,"depth":145,"links":14868},[14869,14870,14871,14876,14883],{"id":13643,"depth":104,"text":13643},{"id":13749,"depth":104,"text":13749},{"id":13775,"depth":104,"text":13775,"children":14872},[14873,14874,14875],{"id":13778,"depth":145,"text":13779},{"id":13796,"depth":145,"text":13797},{"id":13994,"depth":145,"text":13995},{"id":14432,"depth":104,"text":14432,"children":14877},[14878,14879,14881,14882],{"id":14435,"depth":145,"text":14436},{"id":14502,"depth":145,"text":14880},"坑 2：render.photo 是 (props, context) 双参签名",{"id":14704,"depth":145,"text":14705},{"id":14834,"depth":145,"text":14835},{"id":14852,"depth":104,"text":14852},"记录在 Magic Img 生图图库中引入 react-photo-album 实现 Masonry 瀑布流的过程：技术选型背景、需求梳理、安装使用，以及「忘记引入样式表导致一张一列」等踩坑与排查方法。","\u002Fposts\u002Freact-photo-album-masonry\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Freact-photo-album-masonry",{"title":13638,"description":14884},"前端工程实践","posts\u002Freact-photo-album-masonry\u002Findex",[10918,13726,14892,14893,14894],"瀑布流","前端","Tailwind","-O-kAQLEyRzwVhOpuo-yBxZim9LtvhChPkpP92ipyjM",1786887349178]