[{"data":1,"prerenderedAt":4900},["ShallowReactive",2],{"site-settings":3,"writing-\u002Fwriting\u002F2026-07-29-etf-premium-discount-cost-of-carry":103,"writing-navigation":580},{"site":4,"home":19,"writing":41,"services":49,"about":84},{"id":5,"title":6,"description":7,"extension":8,"footer":9,"location":13,"meta":14,"name":6,"stem":15,"theme":16,"__hash__":18},"site\u002Fsettings\u002Fsite.yml","Quilibra","这是我自己部署的小主页，用于记录想法并提供一些服务的入口。","yml",{"icp":10,"policeRecord":11,"credit":12},"皖ICP备2026023522号-1","皖公网安备34012102000966号","DESIGNED & BUILT BY GPT-5.6 IN CODEX","China · UTC+8",{},"settings\u002Fsite",{"defaultAccent":17},"#000000","jtRrbdN47gDyGs_u2ubGZhFFByRvoiraZTjokx9SF0A",{"id":20,"dock":21,"extension":8,"headline":25,"introduction":28,"kicker":29,"latestWriting":30,"meta":36,"modules":37,"stem":38,"systemsLabel":39,"__hash__":40},"homeSettings\u002Fsettings\u002Fhome.yml",{"writing":22,"services":23,"about":24},"文章","服务","关于",{"prefix":26,"emphasis":27},"你好，这里是","Equilibra","这是我自己部署的个人主页，用于记录我的一些事情并维护我的一些服务。","个人主页 \u002F 持续施工中",{"enabled":31,"title":32,"linkLabel":33,"emptyLabel":34,"limit":35},true,"最近写下","全部文章","暂无文章",3,{},{"services":31,"rgbAssembly":31},"settings\u002Fhome","SYSTEMS","mLQiZfULI1ARi0lqmzbNtwFTadrRDtSbuzkF2UyyPzU",{"id":42,"emptyLabel":43,"extension":8,"intro":44,"meta":45,"seoDescription":46,"stem":47,"__hash__":48},"writingSettings\u002Fsettings\u002Fwriting.yml","没有找到匹配的文章。","随笔、学习、交易，以及一些观察。",{},"随笔、学习、交易与观察。","settings\u002Fwriting","YEMHVxKRvAlcMP_kI5F46TdJR19p9ErI69cG5BbP_j8",{"id":50,"accessNotes":51,"accessTitle":54,"extension":8,"intro":55,"items":56,"meta":80,"seoDescription":81,"stem":82,"__hash__":83},"servicesSettings\u002Fsettings\u002Fservices.yml",[52,53],"公开入口由 VPS 上的 Nginx 提供 HTTPS 与反向代理。","家中服务通过私网连接，不直接暴露 。","访问说明","统一入口，分别运行。尚未开放的服务会在域名和登录保护完成后上线。",[57,63,69,75],{"name":58,"description":59,"href":60,"state":61,"icon":62,"showOnHome":31},"Code","浏览器里的开发环境","https:\u002F\u002Fcode.quilibra.cn","online","code",{"name":64,"description":65,"href":66,"state":67,"icon":68,"showOnHome":31},"Photos","照片备份和浏览","https:\u002F\u002Fphotos.example.com","soon","image",{"name":70,"description":71,"href":72,"state":73,"icon":74,"showOnHome":31},"NAS","文件与存储管理","https:\u002F\u002Fnas.example.com","private","hard-drive",{"name":76,"description":77,"href":78,"state":61,"icon":79,"showOnHome":31},"Status","服务可用性与故障记录","#","activity",{},"开发环境、相册、NAS 和运行状态。","settings\u002Fservices","yLhlQNGotM4SYxIDWlMMScw4erJoBdsGRFf_uffFabk",{"id":85,"contactCopy":86,"contactLabel":87,"currentFocus":88,"extension":8,"focusLabel":89,"introduction":90,"meta":91,"seoDescription":92,"stem":93,"topicsLabel":94,"writingTopics":95,"__hash__":102},"aboutSettings\u002Fsettings\u002Fabout.yml","不是，你联系我干嘛？","联系","正在上班当牛马。","正在做的事","这是我自己部署的个人主页，用于记录我的想法并维护我的一些服务。",{},"一些基本信息，以及这个网站目前用来做什么。","settings\u002Fabout","关注的主题",[96,97,98,99,100,101],"看盘看线","捣鼓电脑","折腾NAS","踢踢野球","看看科普","玩币练胆","ZVRx6CC1-GDQUGPl4C4XUfgy30ovFv6LdaXNegQjWAo",{"id":104,"title":105,"body":106,"category":558,"date":559,"description":560,"extension":561,"hidden":562,"meta":563,"navigation":31,"path":564,"publishedAt":565,"seo":566,"slug":567,"stem":568,"tags":569,"updated":578,"__hash__":579},"writing\u002Fwriting\u002F2026-07-29-etf-premium-discount-cost-of-carry.md","从ETF到商品期货：申赎机制、折溢价与持有成本模型",{"type":107,"value":108,"toc":536},"minimark",[109,114,118,122,139,142,145,148,208,211,214,228,239,242,256,259,263,266,273,276,300,303,306,326,329,340,343,346,350,353,356,372,375,401,407,410,415,418,422,473,476,496,499,513,516,522,525,532],[110,111,113],"h2",{"id":112},"etf与lof申赎机制决定折溢价","ETF与LOF：申赎机制决定折溢价",[115,116,117],"h3",{"id":117},"底层机制差异",[119,120,121],"p",{},"ETF和LOF虽然都是场内可交易的开放式基金，但申赎机制截然不同：",[123,124,125,133],"ul",{},[126,127,128,132],"li",{},[129,130,131],"strong",{},"ETF：实物申赎，T+0瞬时套利。"," 机构投资者用一篮子股票换取基金份额，最小申赎单位通常为50万或100万份，门槛在百万级别。每日盘前基金公司公布PCF（Portfolio Composition File）清单，列明当日申赎对应的一篮子股票明细。",[126,134,135,138],{},[129,136,137],{},"LOF：现金申赎，跨系统转托管需T+2日。"," 门槛低（1份起），普通投资者即可参与，但套利存在时滞和净值波动风险。",[119,140,141],{},"这个差异是两者折溢价收敛速度天差地别的根本原因。",[115,143,144],{"id":144},"现金替代的四种标志",[119,146,147],{},"ETF实物申赎中，并非每只成分股都能完美交收。PCF清单对每只股票标注现金替代标志：",[149,150,151,164],"table",{},[152,153,154],"thead",{},[155,156,157,161],"tr",{},[158,159,160],"th",{},"标志",[158,162,163],{},"含义",[165,166,167,178,188,198],"tbody",{},[155,168,169,175],{},[170,171,172],"td",{},[129,173,174],{},"禁止",[170,176,177],{},"必须用股票实物交收",[155,179,180,185],{},[170,181,182],{},[129,183,184],{},"允许",[170,186,187],{},"申购可现金替代，赎回必须给股票，多退少补",[155,189,190,195],{},[170,191,192],{},[129,193,194],{},"必须",[170,196,197],{},"申赎均必须用现金，针对停牌或待剔除股票",[155,199,200,205],{},[170,201,202],{},[129,203,204],{},"退补",[170,206,207],{},"必须现金替代，按基金公司实际买卖成本多退少补",[115,209,210],{"id":210},"折溢价的形成",[119,212,213],{},"场内基金有两个价格：",[123,215,216,222],{},[126,217,218,221],{},[129,219,220],{},"IOPV（参考净值）","：实时计算的基金份额参考价值",[126,223,224,227],{},[129,225,226],{},"市价","：二级市场交易价格",[119,229,230,231,234,235,238],{},"当市价 > IOPV，即为",[129,232,233],{},"溢价","；反之则为",[129,236,237],{},"折价","。",[119,240,241],{},"套利机制是折溢价收敛的驱动力：",[123,243,244,250],{},[126,245,246,249],{},[129,247,248],{},"溢价套利","：T日买入一篮子股票 → 申购ETF份额 → 卖出ETF，全流程T+0完成",[126,251,252,255],{},[129,253,254],{},"折价套利","：T日买入ETF → 赎回为一篮子股票 → 卖出股票，同样T+0",[119,257,258],{},"正因为ETF套利链条畅通，折溢价通常控制在1%以内。而LOF因T+2转托管时滞，套利者需承担两天净值波动风险，折溢价常达5%以上。",[110,260,262],{"id":261},"跨境etf全现金替代与qdii额度约束","跨境ETF：全现金替代与QDII额度约束",[115,264,265],{"id":265},"一个常见误区",[119,267,268,269,272],{},"\"我有了一篮子股票，如何申购跨境ETF？\"——这个问题本身存在误区。跨境ETF的成分股在境外，境内投资者无法持有和交收这些股票，因此",[129,270,271],{},"跨境ETF不能实物申购，只能全现金替代","。投资者交付现金，由基金公司代为买入境外成分股。",[115,274,275],{"id":275},"现金申购流程",[277,278,279,282,285,288,291,294,297],"ol",{},[126,280,281],{},"确认券商具备跨境ETF申赎资格",[126,283,284],{},"T日盘前查阅PCF清单（现金替代标志全为\"退补\"）",[126,286,287],{},"核对当日QDII额度与开放状态（可能限购，极端情况单日仅100元）",[126,289,290],{},"计算所需现金（现金替代款 + 预估现金差额），最小单位50万份",[126,292,293],{},"T日交易时段通过券商端提交委托",[126,295,296],{},"T+1日日终份额确认，T+2日可用",[126,298,299],{},"基金公司代买完成后，进行现金差额多退少补",[115,301,302],{"id":302},"三层结构与溢价固化",[119,304,305],{},"跨境ETF的运作涉及三层主体：",[123,307,308,314,320],{},[126,309,310,313],{},[129,311,312],{},"外汇局","：审批QDII额度（总量约1708亿美元）",[126,315,316,319],{},[129,317,318],{},"基金公司","：在额度内换汇，代投资者买入境外成分股",[126,321,322,325],{},[129,323,324],{},"境内投资者","：只能通过现金提交申购申请",[119,327,328],{},"当额度充裕时，套利机制正常运行。但当额度耗尽时：",[277,330,331,334,337],{},[126,332,333],{},"基金公司暂停或限额申购（单日低至10元）",[126,335,336],{},"一级市场申购通道堵死，套利者无法创造新份额",[126,338,339],{},"二级市场资金只能抢存量份额，溢价飙升至10%-50%+",[119,341,342],{},"此外，现金申赎 + 代买模式下，跨时区汇率波动和境外开盘价差导致退补款不可预知，套利收益无法锁定，进一步抑制套利者进场意愿，固化高溢价。",[119,344,345],{},"典型场景：QDII ETF因外汇额度受限，一级市场申购通道关闭，套利链条断裂，溢价长期高达10%-40%。",[110,347,349],{"id":348},"商品期货持有成本模型与期限结构","商品期货：持有成本模型与期限结构",[115,351,352],{"id":352},"持有成本定价公式",[119,354,355],{},"商品期货的理论价格由持有成本模型决定：",[357,358,363],"pre",{"className":359,"code":360,"language":361,"meta":362,"style":362},"language-plain shiki shiki-themes github-light github-dark","F = S · e^((r + u − y) · T)\n","plain","",[62,364,365],{"__ignoreMap":362},[366,367,370],"span",{"class":368,"line":369},"line",1,[366,371,360],{},[119,373,374],{},"其中：",[123,376,377,383,389,395],{},[126,378,379,382],{},[129,380,381],{},"S","：现货价格",[126,384,385,388],{},[129,386,387],{},"r","：无风险利率（资金占用成本）",[126,390,391,394],{},[129,392,393],{},"u","：仓储保险成本率",[126,396,397,400],{},[129,398,399],{},"y","：便利收益率（convenience yield）",[119,402,403,406],{},[129,404,405],{},"便利收益","是持有实物库存的隐性收益——当供应紧张时，手上有货意味着生产不断线、客户不流失，这种\"随时可用\"的价值就是便利收益。它与库存水平成反比：库存越低，便利收益越高。",[115,408,409],{"id":409},"基差及其收敛",[119,411,412],{},[129,413,414],{},"基差 = 现货价格(S) − 期货价格(F)",[119,416,417],{},"随着交割日临近，持仓成本（r+u−y）T逐渐衰减趋零，期货价格向现货价格收敛，基差必然趋近于零。否则将触发无风险套利：买低卖高，交割平仓。",[115,419,421],{"id":420},"期限结构contango与backwardation","期限结构：Contango与Backwardation",[149,423,424,439],{},[152,425,426],{},[155,427,428,431,434,437],{},[158,429,430],{},"结构",[158,432,433],{},"形态",[158,435,436],{},"成因",[158,438,163],{},[165,440,441,457],{},[155,442,443,448,451,454],{},[170,444,445],{},[129,446,447],{},"Contango",[170,449,450],{},"S \u003C F近 \u003C F远",[170,452,453],{},"r+u > y，库存充足",[170,455,456],{},"常态结构，持有成本主导",[155,458,459,464,467,470],{},[170,460,461],{},[129,462,463],{},"Backwardation",[170,465,466],{},"S > F近 > F远",[170,468,469],{},"r+u \u003C y，库存紧张",[170,471,472],{},"供应短缺，现货溢价",[115,474,475],{"id":475},"品种实例",[123,477,478,484,490],{},[126,479,480,483],{},[129,481,482],{},"黄金","：投资性商品，便利收益y≈0，恒为Contango结构",[126,485,486,489],{},[129,487,488],{},"原油","：消费性商品，便利收益波动剧烈，常在Contango与Backwardation间切换。紧缺时深度Backwardation，过剩时转Contango",[126,491,492,495],{},[129,493,494],{},"农产品","：受季节性收获周期影响，仓储成本u与便利收益y交替主导曲线形态",[115,497,498],{"id":498},"套利操作",[123,500,501,507],{},[126,502,503,506],{},[129,504,505],{},"正向套利（Cash and Carry）","：当F实际 > F理论时，借钱买入现货 + 卖出期货，锁定差价",[126,508,509,512],{},[129,510,511],{},"反向套利（Reverse Cash and Carry）","：当F实际 \u003C F理论时，卖空现货 + 买入期货。但对消费性商品，卖空现货难以操作",[115,514,515],{"id":515},"远期与期货的凸性调整",[119,517,518,519,238],{},"期货合约每日盯市（mark-to-market），而远期合约到期才结算。当标的资产价格与利率正相关时，期货价格略高于远期价格，这一差异称为",[129,520,521],{},"凸性调整",[110,523,524],{"id":524},"串联",[119,526,527,528,531],{},"三个主题看似分散，实则共享一条逻辑主线：",[129,529,530],{},"机制决定效率，效率决定价格形态","。ETF的T+0实物申赎使折溢价瞬间收敛；跨境ETF的额度瓶颈使套利链条断裂、溢价固化；商品期货的持有成本模型则将仓储、利率、库存预期统一为一个数学框架，解释了曲线形态的成因。理解机制本身，比记住某个具体结论更重要。",[533,534,535],"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);}",{"title":362,"searchDepth":537,"depth":537,"links":538},2,[539,544,549,557],{"id":112,"depth":537,"text":113,"children":540},[541,542,543],{"id":117,"depth":35,"text":117},{"id":144,"depth":35,"text":144},{"id":210,"depth":35,"text":210},{"id":261,"depth":537,"text":262,"children":545},[546,547,548],{"id":265,"depth":35,"text":265},{"id":275,"depth":35,"text":275},{"id":302,"depth":35,"text":302},{"id":348,"depth":537,"text":349,"children":550},[551,552,553,554,555,556],{"id":352,"depth":35,"text":352},{"id":409,"depth":35,"text":409},{"id":420,"depth":35,"text":421},{"id":475,"depth":35,"text":475},{"id":498,"depth":35,"text":498},{"id":515,"depth":35,"text":515},{"id":524,"depth":537,"text":524},"学习","2026-07-29","ETF实物申赎与LOF现金申赎的机制差异决定折溢价收敛速度，跨境ETF的QDII额度约束固化高溢价，商品期货持有成本模型统一解释期限结构形态。","md",false,{},"\u002Fwriting\u002F2026-07-29-etf-premium-discount-cost-of-carry","2026-07-29T10:51:17+08:00",{"title":105,"description":560},"etf-premium-discount-cost-of-carry","writing\u002F2026-07-29-etf-premium-discount-cost-of-carry",[570,571,572,573,574,575,576,577],"ETF","LOF","跨境ETF","QDII","商品期货","持有成本模型","期限结构","折溢价",null,"P-nL6PDPMB-K1dYMhb05-mjrddhVCF2rRa7sbCY8CXo",[581,896,4360,4526,4720],{"id":104,"title":105,"body":582,"category":558,"date":559,"description":560,"extension":561,"hidden":562,"meta":893,"navigation":31,"path":564,"publishedAt":565,"seo":894,"slug":567,"stem":568,"tags":895,"updated":578,"__hash__":579},{"type":107,"value":583,"toc":872},[584,586,588,590,600,602,604,606,650,652,654,664,670,672,682,684,686,688,692,694,710,712,714,728,730,738,740,742,744,746,748,756,758,776,780,782,786,788,790,830,832,846,848,858,860,864,866,870],[110,585,113],{"id":112},[115,587,117],{"id":117},[119,589,121],{},[123,591,592,596],{},[126,593,594,132],{},[129,595,131],{},[126,597,598,138],{},[129,599,137],{},[119,601,141],{},[115,603,144],{"id":144},[119,605,147],{},[149,607,608,616],{},[152,609,610],{},[155,611,612,614],{},[158,613,160],{},[158,615,163],{},[165,617,618,626,634,642],{},[155,619,620,624],{},[170,621,622],{},[129,623,174],{},[170,625,177],{},[155,627,628,632],{},[170,629,630],{},[129,631,184],{},[170,633,187],{},[155,635,636,640],{},[170,637,638],{},[129,639,194],{},[170,641,197],{},[155,643,644,648],{},[170,645,646],{},[129,647,204],{},[170,649,207],{},[115,651,210],{"id":210},[119,653,213],{},[123,655,656,660],{},[126,657,658,221],{},[129,659,220],{},[126,661,662,227],{},[129,663,226],{},[119,665,230,666,234,668,238],{},[129,667,233],{},[129,669,237],{},[119,671,241],{},[123,673,674,678],{},[126,675,676,249],{},[129,677,248],{},[126,679,680,255],{},[129,681,254],{},[119,683,258],{},[110,685,262],{"id":261},[115,687,265],{"id":265},[119,689,268,690,272],{},[129,691,271],{},[115,693,275],{"id":275},[277,695,696,698,700,702,704,706,708],{},[126,697,281],{},[126,699,284],{},[126,701,287],{},[126,703,290],{},[126,705,293],{},[126,707,296],{},[126,709,299],{},[115,711,302],{"id":302},[119,713,305],{},[123,715,716,720,724],{},[126,717,718,313],{},[129,719,312],{},[126,721,722,319],{},[129,723,318],{},[126,725,726,325],{},[129,727,324],{},[119,729,328],{},[277,731,732,734,736],{},[126,733,333],{},[126,735,336],{},[126,737,339],{},[119,739,342],{},[119,741,345],{},[110,743,349],{"id":348},[115,745,352],{"id":352},[119,747,355],{},[357,749,750],{"className":359,"code":360,"language":361,"meta":362,"style":362},[62,751,752],{"__ignoreMap":362},[366,753,754],{"class":368,"line":369},[366,755,360],{},[119,757,374],{},[123,759,760,764,768,772],{},[126,761,762,382],{},[129,763,381],{},[126,765,766,388],{},[129,767,387],{},[126,769,770,394],{},[129,771,393],{},[126,773,774,400],{},[129,775,399],{},[119,777,778,406],{},[129,779,405],{},[115,781,409],{"id":409},[119,783,784],{},[129,785,414],{},[119,787,417],{},[115,789,421],{"id":420},[149,791,792,804],{},[152,793,794],{},[155,795,796,798,800,802],{},[158,797,430],{},[158,799,433],{},[158,801,436],{},[158,803,163],{},[165,805,806,818],{},[155,807,808,812,814,816],{},[170,809,810],{},[129,811,447],{},[170,813,450],{},[170,815,453],{},[170,817,456],{},[155,819,820,824,826,828],{},[170,821,822],{},[129,823,463],{},[170,825,466],{},[170,827,469],{},[170,829,472],{},[115,831,475],{"id":475},[123,833,834,838,842],{},[126,835,836,483],{},[129,837,482],{},[126,839,840,489],{},[129,841,488],{},[126,843,844,495],{},[129,845,494],{},[115,847,498],{"id":498},[123,849,850,854],{},[126,851,852,506],{},[129,853,505],{},[126,855,856,512],{},[129,857,511],{},[115,859,515],{"id":515},[119,861,518,862,238],{},[129,863,521],{},[110,865,524],{"id":524},[119,867,527,868,531],{},[129,869,530],{},[533,871,535],{},{"title":362,"searchDepth":537,"depth":537,"links":873},[874,879,884,892],{"id":112,"depth":537,"text":113,"children":875},[876,877,878],{"id":117,"depth":35,"text":117},{"id":144,"depth":35,"text":144},{"id":210,"depth":35,"text":210},{"id":261,"depth":537,"text":262,"children":880},[881,882,883],{"id":265,"depth":35,"text":265},{"id":275,"depth":35,"text":275},{"id":302,"depth":35,"text":302},{"id":348,"depth":537,"text":349,"children":885},[886,887,888,889,890,891],{"id":352,"depth":35,"text":352},{"id":409,"depth":35,"text":409},{"id":420,"depth":35,"text":421},{"id":475,"depth":35,"text":475},{"id":498,"depth":35,"text":498},{"id":515,"depth":35,"text":515},{"id":524,"depth":537,"text":524},{},{"title":105,"description":560},[570,571,572,573,574,575,576,577],{"id":897,"title":898,"body":899,"category":558,"date":4345,"description":4346,"extension":561,"hidden":562,"meta":4347,"navigation":31,"path":4348,"publishedAt":4349,"seo":4350,"slug":4351,"stem":4352,"tags":4353,"updated":4358,"__hash__":4359},"writing\u002Fwriting\u002F2026-07-26-quilibra-complete-technical-handbook.md","Quilibra 个人主页完整技术手册：从页面架构到内容发布与生产部署",{"type":107,"value":900,"toc":4273},[901,904,907,928,932,935,938,946,962,976,979,983,986,1006,1009,1012,1016,1019,1113,1116,1122,1128,1142,1146,1149,1155,1158,1191,1206,1210,1216,1250,1268,1279,1285,1331,1338,1342,1345,1409,1428,1435,1470,1483,1487,1493,1525,1543,1582,1585,1596,1600,1603,1626,1645,1664,1667,1671,1674,1702,1705,1709,1716,1773,1784,1787,1813,1816,1822,1828,1832,1843,1849,1856,1866,1889,1893,1903,1906,1926,1929,1969,1975,1979,1990,1993,1996,2010,2031,2037,2061,2072,2076,2087,2090,2117,2128,2131,2146,2150,2153,2169,2172,2178,2192,2199,2217,2221,2231,2238,2241,2255,2269,2276,2280,2291,2297,2305,2311,2318,2322,2325,2411,2414,2420,2447,2470,2483,2493,2497,2506,2509,2531,2534,2542,2548,2554,2561,2568,2582,2586,2593,2596,2619,2638,2641,2645,2652,2658,2667,2680,2684,2687,2693,2696,2702,2705,2711,2714,2720,2726,2738,2770,2777,2783,2808,2811,2816,2825,2830,2842,2862,2866,2872,2878,2891,2908,2917,2936,2939,2945,2949,2985,3050,3056,3059,3065,3076,3087,3091,3142,3153,3159,3163,3166,3172,3175,3217,3237,3248,3259,3263,3266,3270,3276,3307,3311,3317,3351,3355,3361,3399,3402,3405,3409,3412,3429,3436,3439,3442,3483,3488,3492,3495,3502,3519,3522,3526,3530,3539,3542,3551,3554,3563,3566,3580,3583,3596,3599,3606,3609,3623,3626,3632,3636,3640,3659,3662,3665,3692,3697,3701,3781,3790,3793,3883,3886,3889,3917,3928,3932,3936,3959,3962,3978,3982,3993,3996,4004,4007,4013,4017,4027,4031,4040,4050,4054,4060,4064,4067,4073,4076,4082,4092,4096,4099,4102,4105,4108,4111,4117,4120,4124,4127,4181,4184,4204,4207,4210,4270],[119,902,903],{},"这是一份给自己看的项目说明书。它主要以 2026 年 7 月 27 日的实际代码和 VPS 配置为准，从浏览器里看到的页面一直追到内容文件、Git 分支、构建产物、静态服务器和版本化 release。以后忘了某段代码为什么存在、主页颜色为什么能保存、CMS 保存为什么只出现在测试站，或者 3000、3100、3101 三个端口分别在做什么，可以从这里重新建立完整认识。",[119,905,906],{},"本文记录的是一个具体版本的实现，不是一套永远不变的规范。最可靠的事实来源始终是仓库中的代码；本文的作用，是把分散在 Vue、CSS、YAML、脚本和运维文件里的设计意图串起来。",[908,909,910],"blockquote",{},[119,911,912,913,916,917,920,921,924,925,927],{},"2026 年 8 月 3 日更新：网站已经移除长期 ",[62,914,915],{},"staging"," 分支、远程测试站和 ",[62,918,919],{},"promote"," 流程，改为 CMS 直接提交 ",[62,922,923],{},"master","、代码在本地功能分支验证后推送 ",[62,926,923],{},"。下文关于双环境发布的内容保留为历史架构记录，当前操作以仓库 README 和运维手册为准。",[110,929,931],{"id":930},"一先用一句话理解整个网站","一、先用一句话理解整个网站",[119,933,934],{},"Quilibra（名字也是AI起的捏） 是一个由 Nuxt 生成的静态个人网站：页面结构写在 Vue 中，主页设置写在 YAML 中，文章写在 Markdown 中；发布时把这些源文件编译成普通 HTML、CSS、JavaScript 和图片，再由一个很小的 Node 静态服务器提供给浏览器。",[119,936,937],{},"它的静态生成核心是：",[357,939,944],{"className":940,"code":942,"language":943,"meta":362},[941],"language-text","Vue \u002F CSS \u002F YAML \u002F Markdown\n           |\n           | Nuxt generate\n           v\n      .output\u002Fpublic\n           |\n           | 复制到带时间戳的 release\n           v\n  releases\u002F\u003Crelease-id>\n           |\n           | 原子切换 current 符号链接\n           v\n     current -> 当前 release\n           |\n           | Node 静态服务器 :3000\n           v\n        浏览器 \u002F 反向代理\n","text",[62,945,942],{"__ignoreMap":362},[119,947,948,949,951,952,955,956,951,958,961],{},"现在这套链路有两份互不覆盖的实例：",[62,950,923],{}," 构建到 ",[62,953,954],{},"~\u002F.local\u002Fshare\u002Fpersonal-site\u002Fcurrent"," 并由 3000 提供正式站，",[62,957,915],{},[62,959,960],{},"~\u002F.local\u002Fshare\u002Fpersonal-site-staging\u002Fcurrent"," 并由 3100 提供测试站。两边都使用相同的生成和原子切换逻辑，但仓库、成功状态、release 目录和服务端口完全独立。",[119,963,964,965],{},"这里最重要的边界是：",[129,966,967,968,971,972,975],{},"Git 分支中的源码是长期事实，",[62,969,970],{},".output\u002Fpublic"," 是可重新生成的临时产物，各环境 ",[62,973,974],{},"current"," 指向的 release 才是该环境当前真正提供的版本。",[119,977,978],{},"因此，修改 Markdown 或 YAML 只改变源码；只有重新生成并切换 release，线上页面才会改变。网站没有生产数据库，也没有在访问文章时临时渲染 Markdown。",[110,980,982],{"id":981},"二为什么选择静态优先","二、为什么选择静态优先",[119,984,985],{},"个人主页的读取远多于写入，而且文章、简介和服务入口都不需要按访客实时计算。静态生成正好适合这种负载：",[123,987,988,991,994,997,1000,1003],{},[126,989,990],{},"访问时不查询数据库，也不运行 Vue 服务端渲染逻辑；",[126,992,993],{},"HTML 已经在发布阶段生成，首屏可以直接返回完整内容；",[126,995,996],{},"生产服务只需要读取文件，故障面比常驻 Nuxt 服务小；",[126,998,999],{},"每个 release 都是一份独立、完整、可回滚的目录；",[126,1001,1002],{},"Markdown 和 YAML 都在 Git 中，内容、配置和代码可以一起审查与恢复；",[126,1004,1005],{},"CMS 只是源码编辑器，不会成为网站运行时依赖。",[119,1007,1008],{},"代价也很明确：每次内容变化都要重新生成站点；文章越多，生成时间越长；需要登录、评论、实时状态或用户数据时，必须接入独立服务，而不能假装静态文件可以承担动态业务。",[119,1010,1011],{},"当前项目主动接受这个取舍，因为稳定、透明和容易备份比“保存后数据库立刻生效”更重要。",[110,1013,1015],{"id":1014},"三技术栈与各自职责","三、技术栈与各自职责",[119,1017,1018],{},"当前主要依赖如下：",[149,1020,1021,1031],{},[152,1022,1023],{},[155,1024,1025,1028],{},[158,1026,1027],{},"技术",[158,1029,1030],{},"当前职责",[165,1032,1033,1041,1049,1057,1065,1073,1081,1089,1097,1105],{},[155,1034,1035,1038],{},[170,1036,1037],{},"Nuxt 4",[170,1039,1040],{},"应用框架、路由、预渲染、页面头信息和构建产物",[155,1042,1043,1046],{},[170,1044,1045],{},"Vue 3",[170,1047,1048],{},"组件、响应式状态、生命周期和浏览器交互",[155,1050,1051,1054],{},[170,1052,1053],{},"TypeScript",[170,1055,1056],{},"页面、组件、配置、内容脚本和测试的静态类型检查",[155,1058,1059,1062],{},[170,1060,1061],{},"Nuxt Content 3",[170,1063,1064],{},"读取 YAML\u002FMarkdown、建立集合、查询文章并渲染正文",[155,1066,1067,1070],{},[170,1068,1069],{},"Zod",[170,1071,1072],{},"约束站点设置与文章字段",[155,1074,1075,1078],{},[170,1076,1077],{},"Lucide Vue",[170,1079,1080],{},"返回、搜索、服务、调色等界面图标",[155,1082,1083,1086],{},[170,1084,1085],{},"原生 CSS",[170,1087,1088],{},"全部视觉、响应式布局、悬浮反馈和字标动画",[155,1090,1091,1094],{},[170,1092,1093],{},"Sveltia CMS",[170,1095,1096],{},"在浏览器中编辑仓库里的 YAML、Markdown 和上传资源",[155,1098,1099,1102],{},[170,1100,1101],{},"Playwright",[170,1103,1104],{},"真实浏览器中的交互、布局、响应式和回归测试",[155,1106,1107,1110],{},[170,1108,1109],{},"Node.js",[170,1111,1112],{},"内容工具、同步与发布脚本、生产和测试静态服务器",[119,1114,1115],{},"没有引入 Tailwind、组件库、动画框架、运行时数据库或外部字体。页面使用系统无衬线字体和系统等宽字体回退，这减少了字体请求，也避免了字体加载完成时重新排版。",[119,1117,1118,1121],{},[62,1119,1120],{},"package.json"," 中最常用的入口是：",[357,1123,1126],{"className":1124,"code":1125,"language":943,"meta":362},[941],"npm run dev              本地临时 Nuxt 开发服务\nnpm run generate         生成静态站点\nnpm run typecheck        Nuxt\u002FVue TypeScript 检查\nnpm run content:validate 校验全部文章与站点设置\nnpm run test:content     内容校验器与受限 writer 的单元测试\nnpm run test:release     release 工具的单元测试\nnpm run test:e2e         Playwright 浏览器测试\nnpm run publish          发布未提交的受管内容\nnpm run deploy           部署已经提交且工作区干净的代码版本\nnpm run sync             拉取指定 GitHub 分支并自动检查、构建和部署\nnpm run promote          把测试站确认过的 CMS 内容安全发布到生产\nnpm run rollback         切换回指定历史 release\n",[62,1127,1125],{"__ignoreMap":362},[119,1129,1130,1133,1134,1137,1138,1141],{},[62,1131,1132],{},"build"," 与 ",[62,1135,1136],{},"preview"," 仍是 Nuxt 的常规命令，但当前生产链路以 ",[62,1139,1140],{},"generate"," 和自有静态服务器为准。",[110,1143,1145],{"id":1144},"四目录结构与所有权边界","四、目录结构与所有权边界",[119,1147,1148],{},"理解这个项目，先要知道“改什么就去哪里”。",[357,1150,1153],{"className":1151,"code":1152,"language":943,"meta":362},[941],"personal-site\u002F\n├── app\u002F\n│   ├── app.vue                         全站外壳、主题初始化、动态 favicon\n│   ├── assets\u002Fcss\u002Fmain.css             全站视觉与响应式规则\n│   ├── components\u002F\n│   │   ├── ArtisticWordmark.vue        可编辑艺术字与重播控制\n│   │   ├── RgbAssembly.vue             RGB 滑杆、预设与重置\n│   │   ├── ServiceIcon.vue             服务图标名称到 Lucide 的映射\n│   │   └── ServiceStatusDot.vue        服务状态点\n│   ├── composables\u002F\n│   │   ├── useAccentTheme.ts           主题色状态、混色、对比色与持久化\n│   │   ├── useSiteSettings.ts          并行读取并组合五份站点设置\n│   │   └── useWordmarkAnimation.ts     跨组件字标重播信号\n│   ├── config\u002F                         文章分类、服务状态和图标枚举\n│   ├── pages\u002F                          首页、文章、服务、关于页面\n│   └── error.vue                       错误页面\n├── content\u002F\n│   ├── settings\u002F                       全站与四个页面的独立 YAML 配置\n│   └── writing\u002F*.md                    正式文章\n├── public\u002F\n│   ├── admin\u002F                          Sveltia CMS 入口与字段配置\n│   ├── brand\u002F                          字标位图资源\n│   ├── uploads\u002F                        文章上传资源\n│   └── favicon.svg                     静态 favicon 回退\n├── scripts\u002F\n│   ├── content\u002Fvalidate.ts             内容结构校验\n│   ├── content\u002Fwrite.ts                受限文章写入、更新与删除\n│   ├── release.ts                      测试、生成、提交、发布和回滚\n│   ├── promote.ts                      staging 内容安全合并到 master\n│   ├── sync.ts                         GitHub 拉取、代理、构建与部署\n│   ├── webhook.ts                      GitHub 签名校验、事件队列与部署触发\n│   └── static-server.mjs               生产与测试静态文件服务器\n├── tests\u002F                              单元、发布和浏览器测试\n├── ops\u002F\n│   ├── openclaw\u002F...\u002FSKILL.md           OpenClaw 文章发布 Skill\n│   ├── nginx\u002F                           正式域名、测试域名和默认拒绝站点\n│   └── systemd\u002F                         静态、Webhook 与同步服务\n├── content.config.ts                   Nuxt Content 集合\n├── content.schema.ts                   站点设置 Schema\n├── nuxt.config.ts                      Nuxt 与静态预渲染配置\n└── playwright.config.ts                E2E 临时服务与浏览器配置\n",[62,1154,1152],{"__ignoreMap":362},[119,1156,1157],{},"可以把这些文件分成四层：",[277,1159,1160,1166,1172,1181],{},[126,1161,1162,1165],{},[62,1163,1164],{},"content\u002F"," 是可频繁修改的数据层。",[126,1167,1168,1171],{},[62,1169,1170],{},"app\u002F"," 是页面表现和交互层。",[126,1173,1174,1133,1177,1180],{},[62,1175,1176],{},"scripts\u002F",[62,1178,1179],{},"ops\u002F"," 是发布和运行层。",[126,1182,1183,1186,1187,1190],{},[62,1184,1185],{},".output\u002F","、",[62,1188,1189],{},".nuxt*"," 是构建缓存或产物，不是手工维护的源码。",[119,1192,1193,1194,1197,1198,1201,1202,1205],{},"CMS 默认只管理 ",[62,1195,1196],{},"content\u002Fsettings\u002F"," 中五份固定 YAML、",[62,1199,1200],{},"content\u002Fwriting\u002F"," 和 ",[62,1203,1204],{},"public\u002Fuploads\u002F","。Vue、CSS、CMS 配置、脚本或测试发生变化时，应当视为代码变更，走完整代码部署流程；设置目录中的未知文件也不会自动进入内容发布白名单。",[110,1207,1209],{"id":1208},"五nuxt-配置与构建数据库","五、Nuxt 配置与构建数据库",[119,1211,1212,1215],{},[62,1213,1214],{},"nuxt.config.ts"," 做了几件关键的事：",[123,1217,1218,1225,1231,1234,1237,1244,1247],{},[126,1219,1220,1221,1224],{},"加载 ",[62,1222,1223],{},"@nuxt\u002Fcontent","；",[126,1226,1227,1228,1224],{},"全局引入 ",[62,1229,1230],{},"app\u002Fassets\u002Fcss\u002Fmain.css",[126,1232,1233],{},"关闭开发工具；",[126,1235,1236],{},"设置中文页面语言和移动端 viewport；",[126,1238,1239,1240,1243],{},"使用 ",[62,1241,1242],{},"out-in"," 页面切换过渡；",[126,1245,1246],{},"允许 Nitro 爬取内部链接并预渲染，遇到错误时直接让构建失败；",[126,1248,1249],{},"把 Nuxt Content 的本地数据库放到与构建目录对应的位置。",[119,1251,1252,1253,1201,1256,1259,1260,1263,1264,1267],{},"Nuxt Content 3 在构建过程中会建立 SQLite 内容索引。开发环境默认使用 ",[62,1254,1255],{},".nuxt",[62,1257,1258],{},".data\u002Fcontent\u002Fcontents.sqlite","；release 构建传入 ",[62,1261,1262],{},"NUXT_BUILD_DIR=.nuxt-release","，E2E 使用 ",[62,1265,1266],{},".nuxt-e2e","。把三种构建目录分开，是为了避免开发服务器、正式生成和 Playwright 同时读写同一个内容缓存。",[119,1269,1270,1271,1274,1275,1278],{},"这里的 SQLite 是",[129,1272,1273],{},"构建时索引","，不是生产文章数据库。它帮助 Nuxt Content 查询 Markdown；静态页面生成完成后，生产服务器只负责输出构建结果。遇到 ",[62,1276,1277],{},"no such table: _content_writing"," 时，通常是本地 Nuxt Content 缓存被并发构建或中断破坏，不代表文章源文件丢失。",[119,1280,1281,1284],{},[62,1282,1283],{},"content.config.ts"," 定义了六个集合：",[123,1286,1287,1309],{},[126,1288,1289,1186,1292,1186,1295,1186,1298,1186,1301,1304,1305,1308],{},[62,1290,1291],{},"site",[62,1293,1294],{},"homeSettings",[62,1296,1297],{},"writingSettings",[62,1299,1300],{},"servicesSettings",[62,1302,1303],{},"aboutSettings","：五个 ",[62,1306,1307],{},"data"," 集合，各自读取同名职责的 YAML；",[126,1310,1311,1314,1315,1318,1319,1322,1323,1326,1327,1330],{},[62,1312,1313],{},"writing","：",[62,1316,1317],{},"page"," 类型，读取 ",[62,1320,1321],{},"writing\u002F**\u002F*.md","，因此每篇文章会拥有可路由的 ",[62,1324,1325],{},"path"," 和可供 ",[62,1328,1329],{},"ContentRenderer"," 使用的正文。",[119,1332,1333,1334,1337],{},"E2E 测试通过 ",[62,1335,1336],{},"NUXT_CONTENT_CWD=tests\u002Ffixtures\u002Fcontent"," 把文章集合切换到固定 fixture，测试不会依赖真实文章的标题和数量，也不会改动生产内容。",[110,1339,1341],{"id":1340},"六站点设置五份-yaml-如何保持页面边界","六、站点设置：五份 YAML 如何保持页面边界",[119,1343,1344],{},"站点设置按后台入口拆成五份文件：",[149,1346,1347,1357],{},[152,1348,1349],{},[155,1350,1351,1354],{},[158,1352,1353],{},"文件",[158,1355,1356],{},"管理内容",[165,1358,1359,1369,1379,1389,1399],{},[155,1360,1361,1366],{},[170,1362,1363],{},[62,1364,1365],{},"site.yml",[170,1367,1368],{},"站点名称、浏览器标题、SEO、位置、默认主题色、备案和署名",[155,1370,1371,1376],{},[170,1372,1373],{},[62,1374,1375],{},"home.yml",[170,1377,1378],{},"首页介绍、标题、最近文章、服务矩阵、RGB 模块和底部入口",[155,1380,1381,1386],{},[170,1382,1383],{},[62,1384,1385],{},"writing.yml",[170,1387,1388],{},"文章列表页介绍、SEO 和空状态",[155,1390,1391,1396],{},[170,1392,1393],{},[62,1394,1395],{},"services.yml",[170,1397,1398],{},"服务列表、状态、图标、主页显示和访问说明",[155,1400,1401,1406],{},[170,1402,1403],{},[62,1404,1405],{},"about.yml",[170,1407,1408],{},"关于页介绍、当前状态、关注主题和联系文案",[119,1410,1411,1412,1415,1416,1419,1420,1423,1424,1427],{},"拆分不只是 CMS 菜单分组。如果多个表单只声明同一 YAML 的部分字段，保存其中一个表单可能覆盖另一个表单未声明的字段；因此每个入口必须拥有独立源文件。",[62,1413,1414],{},"useSiteSettings()"," 使用固定异步数据键 ",[62,1417,1418],{},"site-settings","，通过 ",[62,1421,1422],{},"Promise.all"," 并行查询五个 data collection，再返回结构化的 ",[62,1425,1426],{},"site\u002Fhome\u002Fwriting\u002Fservices\u002Fabout"," 对象。任何一份缺失都会立即抛出 500，而不是让页面带着部分设置运行。",[119,1429,1430,1431,1434],{},"这些配置不是任意 YAML。",[62,1432,1433],{},"content.schema.ts"," 为每个文件提供独立严格 Schema，例如：",[123,1436,1437,1443,1446,1449,1458,1461,1467],{},[126,1438,1439,1440,1224],{},"默认强调色必须是 ",[62,1441,1442],{},"#RRGGBB",[126,1444,1445],{},"最新文章数量只能是 1 到 3；",[126,1447,1448],{},"服务数量只能是 1 到 12；",[126,1450,1451,1452,1186,1454,1186,1456,1224],{},"服务状态只能是 ",[62,1453,61],{},[62,1455,73],{},[62,1457,67],{},[126,1459,1460],{},"图标只能使用代码中已有的枚举；",[126,1462,1463,1464,1466],{},"服务链接只能是 ",[62,1465,78],{}," 或完整的 HTTPS URL；",[126,1468,1469],{},"对象使用严格模式，拼错字段不会被静默忽略。",[119,1471,1472,1475,1476,1478,1479,1482],{},[62,1473,1474],{},"public\u002Fadmin\u002Fconfig.yml"," 的表单选项、",[62,1477,1433],{}," 的运行时约束、",[62,1480,1481],{},"app\u002Fconfig\u002Fsite.ts"," 的 TypeScript 枚举应该同步修改。只改 CMS 下拉框，不改 Schema，保存后会校验失败；只改 Schema，不改组件映射，界面可能没有正确图标。",[110,1484,1486],{"id":1485},"七全站外壳与水合边界","七、全站外壳与水合边界",[119,1488,1489,1492],{},[62,1490,1491],{},"app\u002Fapp.vue"," 是每个路由外面的共同外壳。它负责：",[123,1494,1495,1498,1501,1504,1510,1513,1519,1522],{},[126,1496,1497],{},"在页面加载时取得站点设置；",[126,1499,1500],{},"判断当前是否为主页；",[126,1502,1503],{},"初始化和恢复主题色；",[126,1505,1506,1507,1224],{},"把主题 CSS 变量挂到 ",[62,1508,1509],{},".site-shell",[126,1511,1512],{},"为非主页显示“返回”按钮与主题预设切换按钮；",[126,1514,1515,1516,1224],{},"设置标题模板、站点描述、Open Graph 信息和 ",[62,1517,1518],{},"theme-color",[126,1520,1521],{},"根据当前强调色实时生成 data URL favicon；",[126,1523,1524],{},"显示 Nuxt 页面加载进度条。",[119,1526,1527,1530,1531,1534,1535,1538,1539,1542],{},[62,1528,1529],{},"hydrated"," 初始为 ",[62,1532,1533],{},"false","，在 ",[62,1536,1537],{},"onMounted"," 完成主题恢复和本地监听注册后变成 ",[62,1540,1541],{},"true","，最终反映为：",[357,1544,1548],{"className":1545,"code":1546,"language":1547,"meta":362,"style":362},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"site-shell\" data-hydrated=\"true\">\n","html",[62,1549,1550],{"__ignoreMap":362},[366,1551,1552,1556,1560,1564,1567,1571,1574,1576,1579],{"class":368,"line":369},[366,1553,1555],{"class":1554},"sVt8B","\u003C",[366,1557,1559],{"class":1558},"s9eBZ","div",[366,1561,1563],{"class":1562},"sScJk"," class",[366,1565,1566],{"class":1554},"=",[366,1568,1570],{"class":1569},"sZZnC","\"site-shell\"",[366,1572,1573],{"class":1562}," data-hydrated",[366,1575,1566],{"class":1554},[366,1577,1578],{"class":1569},"\"true\"",[366,1580,1581],{"class":1554},">\n",[119,1583,1584],{},"这个属性既是测试可观察的“客户端已经接管页面”标志，也是字标动画的播放闸门。CSS 动画先建立，但在页面完成水合前保持暂停，防止服务端 HTML 刚显示就消耗掉一段动画，等 JavaScript 接管时只剩最终帧。",[119,1586,1587,1588,1591,1592,1595],{},"非主页的全局返回入口固定回到 ",[62,1589,1590],{},"\u002F","，文本只有“返回”，图标和 ",[62,1593,1594],{},"aria-label"," 补充了方向与完整语义。它与文章正文中的“返回文章”属于不同层级：前者返回网站主页，后者返回文章列表。",[110,1597,1599],{"id":1598},"八主页是一张单屏工作台","八、主页是一张单屏工作台",[119,1601,1602],{},"主页不是按多个营销区块向下滚动，而是一张固定在一个视口高度内的工作台。核心区域包括：",[123,1604,1605,1608,1611,1614,1617,1620,1623],{},[126,1606,1607],{},"顶部站点标识、上海时区时钟、指针坐标和主题按钮；",[126,1609,1610],{},"左上介绍、普通标题前缀与 Quilibra 字标；",[126,1612,1613],{},"右上服务矩阵；",[126,1615,1616],{},"左下最近三篇文章；",[126,1618,1619],{},"右下 RGB 调色器；",[126,1621,1622],{},"底部中央文章、服务、关于三个入口；",[126,1624,1625],{},"两侧备案与署名。",[119,1627,1628,1629,1632,1633,1636,1637,1640,1641,1644],{},"首页在 ",[62,1630,1631],{},"setup"," 阶段并行取得站点设置和最近文章。文章查询按 ",[62,1634,1635],{},"date DESC"," 排序，再按 YAML 中的 ",[62,1638,1639],{},"limit"," 截取。服务矩阵先筛选 ",[62,1642,1643],{},"showOnHome: true","，按 YAML 顺序最多取四个；服务页仍然展示完整列表。",[119,1646,1647,1648,1651,1652,1655,1656,1659,1660,1663],{},"顶部时钟通过 ",[62,1649,1650],{},"Intl.DateTimeFormat"," 固定使用 ",[62,1653,1654],{},"Asia\u002FShanghai","，每 30 秒更新一次。指针坐标不是每次 ",[62,1657,1658],{},"pointermove"," 都直接改 DOM：事件只保存最新坐标，然后在下一帧 ",[62,1661,1662],{},"requestAnimationFrame"," 中统一计算。元素边界在挂载与窗口缩放时缓存，触摸指针被忽略。这样可以减少高频事件中的布局读取和响应式更新。",[119,1665,1666],{},"工作台没有为了装饰引入 canvas 或 WebGL。网格、面板、条纹、阴影和入场效果全部由 CSS 完成，浏览器可以直接合成绝大多数变换。",[110,1668,1670],{"id":1669},"九响应式布局为何不只是按比例缩小","九、响应式布局为何不只是“按比例缩小”",[119,1672,1673],{},"主页有大量绝对定位元素，因此响应式的目标不是把桌面页面机械缩成手机版，而是维护清晰的空间约束：",[123,1675,1676,1683,1686,1689,1692,1699],{},[126,1677,1678,1679,1682],{},"桌面端通过 ",[62,1680,1681],{},"clamp()","、视口高度和稳定的面板高度控制上下关系；",[126,1684,1685],{},"最近文章与 RGB 面板在宽桌面上对齐顶部和底部；",[126,1687,1688],{},"小屏笔记本缩小间距和模块尺寸，仍避免重叠；",[126,1690,1691],{},"768 像素附近的紧凑布局隐藏无法舒适操作的 RGB 面板；",[126,1693,1694,1695,1698],{},"手机端重新排列主页元素，并使用 ",[62,1696,1697],{},"overflow: clip"," 防止装饰或过渡产生横向滚动；",[126,1700,1701],{},"固定格式组件使用明确的 grid 列、最小宽度和高度，标题悬浮时不会重新计算可用宽度。",[119,1703,1704],{},"Playwright 会在 768×1024、1024×768、1440×900 和 3440×1440 上读取真实元素几何信息，验证介绍不压住服务矩阵、上方面板不压住底部导航、RGB 与最近文章对齐、页面没有横向溢出。这里测试的是关系，而不是容易因一个像素改动就失效的整页截图。",[110,1706,1708],{"id":1707},"十主题色从一个-rgb-值扩展成整套界面","十、主题色从一个 RGB 值扩展成整套界面",[119,1710,1711,1712,1715],{},"主题逻辑集中在 ",[62,1713,1714],{},"useAccentTheme.ts","。唯一的核心状态是：",[357,1717,1721],{"className":1718,"code":1719,"language":1720,"meta":362,"style":362},"language-ts shiki shiki-themes github-light github-dark","interface RgbColor {\n  r: number\n  g: number\n  b: number\n}\n","ts",[62,1722,1723,1735,1748,1757,1767],{"__ignoreMap":362},[366,1724,1725,1729,1732],{"class":368,"line":369},[366,1726,1728],{"class":1727},"szBVR","interface",[366,1730,1731],{"class":1562}," RgbColor",[366,1733,1734],{"class":1554}," {\n",[366,1736,1737,1741,1744],{"class":368,"line":537},[366,1738,1740],{"class":1739},"s4XuR","  r",[366,1742,1743],{"class":1727},":",[366,1745,1747],{"class":1746},"sj4cs"," number\n",[366,1749,1750,1753,1755],{"class":368,"line":35},[366,1751,1752],{"class":1739},"  g",[366,1754,1743],{"class":1727},[366,1756,1747],{"class":1746},[366,1758,1760,1763,1765],{"class":368,"line":1759},4,[366,1761,1762],{"class":1739},"  b",[366,1764,1743],{"class":1727},[366,1766,1747],{"class":1746},[366,1768,1770],{"class":368,"line":1769},5,[366,1771,1772],{"class":1554},"}\n",[119,1774,1775,1776,1779,1780,1783],{},"三个内置预设是红、绿、蓝。站点设置里的 ",[62,1777,1778],{},"theme.defaultAccent"," 可以是任意合法十六进制颜色，并不要求与预设一致。当前颜色存放在 Nuxt ",[62,1781,1782],{},"useState"," 中，因此同一客户端的页面切换不会创建互相冲突的主题实例。",[119,1785,1786],{},"主题计算分为几步：",[277,1788,1789,1792,1801,1804,1810],{},[126,1790,1791],{},"把每个通道四舍五入并限制在 0 到 255。",[126,1793,1794,1795,1798,1799,238],{},"生成 CSS ",[62,1796,1797],{},"rgb(r g b)"," 与大写 ",[62,1800,1442],{},[126,1802,1803],{},"把强调色分别与白色、画布色和黑色混合，得到 soft、faint、canvas、grid、strong 等衍生色。",[126,1805,1806,1807,238],{},"计算 sRGB 相对亮度，对比黑白文字的对比度，自动选择更清晰的 ",[62,1808,1809],{},"--accent-ink",[126,1811,1812],{},"把结果以 CSS 变量挂到站点外壳，按钮、悬浮底色、网格、字标和 favicon 共同消费这些变量。",[119,1814,1815],{},"主题变量的意义大致如下：",[357,1817,1820],{"className":1818,"code":1819,"language":943,"meta":362},[941],"--accent          原始强调色\n--accent-ink      强调色背景上的黑或白文字\n--accent-soft     大面积悬浮反馈\n--accent-faint    更浅的面板背景\n--accent-canvas   轻微带主题倾向的页面画布\n--accent-grid     网格与线条色\n--accent-strong   向黑色混合后的强调色\n",[62,1821,1819],{"__ignoreMap":362},[119,1823,1824,1825,1827],{},"这样做比在 CSS 中到处硬编码“红色按钮、浅红背景、深红边框”更可靠。换成极亮或极暗的自定义颜色时，文字对比度仍会自动调整。E2E 会直接把颜色调到纯白和纯黑，验证 ",[62,1826,1809],{}," 分别变成深色和白色。",[110,1829,1831],{"id":1830},"十一主题持久化与旧黄色迁移","十一、主题持久化与旧黄色迁移",[119,1833,1834,1835,1838,1839,1842],{},"客户端挂载时，",[62,1836,1837],{},"restoreSavedAccent()"," 从 ",[62,1840,1841],{},"localStorage"," 读取：",[357,1844,1847],{"className":1845,"code":1846,"language":943,"meta":362},[941],"site-accent-rgb   完整 RGB 对象\nsite-accent       red \u002F green \u002F blue \u002F custom 等名称\n",[62,1848,1846],{"__ignoreMap":362},[119,1850,1851,1852,1855],{},"恢复后，深度监听 ",[62,1853,1854],{},"accentRgb","，后续每次变化都会重新保存。由于服务端无法读取浏览器存储，静态 HTML 首先使用 YAML 中的默认颜色；客户端挂载后再恢复访客上次的选择。",[119,1857,1858,1859,1133,1862,1865],{},"旧版本曾使用黄色默认主题。为了避免已经访问过网站的浏览器永远被旧缓存锁在黄色，恢复逻辑专门识别 ",[62,1860,1861],{},"{255,255,2}",[62,1863,1864],{},"yellow"," 的组合，删除这两个旧值并回到当前 YAML 默认色。这是一次小型数据迁移：不是清空所有人的自定义配色，只清除能够明确识别的旧默认值。",[119,1867,1868,1869,1872,1873,1876,1877,1880,1881,1884,1885,1888],{},"动态 favicon 也跟随主题和站点名称。",[62,1870,1871],{},"app.vue"," 在内存中生成一个包含主题背景色与 ",[62,1874,1875],{},"site.name"," 首字符的 SVG，再编码为 ",[62,1878,1879],{},"data:image\u002Fsvg+xml","。因此 CMS 修改站点名称并重新构建后，标签页字母会同步改变；图标底色、字色和 ",[62,1882,1883],{},"meta[name=theme-color]"," 则继续与 RGB 面板同步。",[62,1886,1887],{},"public\u002Ffavicon.svg"," 只作为静态回退。",[110,1890,1892],{"id":1891},"十二rgb-调色器的交互细节","十二、RGB 调色器的交互细节",[119,1894,1895,1898,1899,1902],{},[62,1896,1897],{},"RgbAssembly.vue"," 不是独立维护另一份颜色，它调用同一个 ",[62,1900,1901],{},"useAccentTheme()","，因此滑杆、预设按钮、页面按钮和 favicon 始终共享状态。",[119,1904,1905],{},"三个 range input 分别控制 R、G、B：",[123,1907,1908,1914,1917,1920,1923],{},[126,1909,1910,1913],{},[62,1911,1912],{},"input"," 事件实时更新颜色，拖动时页面连续渐变；",[126,1915,1916],{},"每条轨道通过 CSS 变量计算已填充百分比；",[126,1918,1919],{},"输出区显示十六进制、十进制通道值，并采用自动对比文字；",[126,1921,1922],{},"点击 RED、GREEN、BLUE 立即加载预设；",[126,1924,1925],{},"重置按钮恢复 YAML 中的默认颜色，而不是写死某个预设。",[119,1927,1928],{},"字标动画不能在滑杆的每一个像素变化时重播，否则会不断重建动画并显得卡顿。组件因此区分“预览变化”和“提交变化”：",[123,1930,1931,1938,1944,1954,1961],{},[126,1932,1933,1934,1937],{},"指针按下后标记 ",[62,1935,1936],{},"pointerActive","，并捕获当前 pointer；",[126,1939,1940,1941,1943],{},"拖动中的 ",[62,1942,1912],{}," 只改变颜色；",[126,1945,1946,1949,1950,1953],{},[62,1947,1948],{},"pointerup"," 或 ",[62,1951,1952],{},"pointercancel"," 才请求重播一次字标；",[126,1955,1956,1957,1960],{},"键盘调整 range 时由 ",[62,1958,1959],{},"change"," 请求重播；",[126,1962,1963,1964,1201,1966,1968],{},"一个零延时抑制标志防止同一次鼠标提交同时触发 ",[62,1965,1948],{},[62,1967,1959],{},"，造成双播。",[119,1970,1971,1972],{},"预设与重置属于离散操作，所以点击时立即换色并重播。这个模型可以概括为：",[129,1973,1974],{},"连续操作实时预览、松手提交动画；离散操作立即提交。",[110,1976,1978],{"id":1977},"十三quilibra-字标是可编辑的艺术字","十三、Quilibra 字标是可编辑的艺术字",[119,1980,1981,1982,1985,1986,1989],{},"当前字标不再依赖固定图片，而是由 ",[62,1983,1984],{},"content\u002Fsettings\u002Fhome.yml"," 中的 ",[62,1987,1988],{},"headline.emphasis"," 生成真实文本。CMS 的“首页 \u002F 艺术字文字”可以直接修改内容，Schema 将长度限制为 20 个字符；组件还会按照中英文字符宽度自动缩小过长文字，避免挤出首屏。",[119,1991,1992],{},"字形使用随构建产物一起发布的 Cormorant Garamond 600 Italic。页面只引入拉丁字符子集，既保留高对比衬线斜体的艺术感，也不依赖 Google Fonts 等站外服务；无法覆盖的字符会回退到本机衬线字体。",[119,1994,1995],{},"动画仍然采用“RGB 套色从偏移到合拢”，没有模拟笔尖逐笔书写。字标由四层相同文本组成：",[123,1997,1998,2001,2004,2007],{},[126,1999,2000],{},"红层从左下附近偏移进入；",[126,2002,2003],{},"绿层从上方偏移进入；",[126,2005,2006],{},"蓝层从右下附近偏移进入；",[126,2008,2009],{},"最终层使用当前主题色并带极轻的阴影。",[119,2011,2012,2013,2016,2017,1186,2020,1201,2023,2026,2027,2030],{},"前三层使用 ",[62,2014,2015],{},"mix-blend-mode: multiply","、动态模糊、透明度和不同位移。动画前段保留可见错版，中段快速靠近，后段降低彩色层透明度并让最终主题层聚焦。四层都只改变 ",[62,2018,2019],{},"opacity",[62,2021,2022],{},"transform",[62,2024,2025],{},"filter","，播放时临时声明 ",[62,2028,2029],{},"will-change","，结束后不长期占用合成资源。",[119,2032,2033,2036],{},[62,2034,2035],{},"ArtisticWordmark.vue"," 的加载控制解决了“首次打开没有动画”和字体中途替换的问题：",[277,2038,2039,2045,2052,2055,2058],{},[126,2040,2041,2044],{},[62,2042,2043],{},"@fontsource\u002Fcormorant-garamond"," 把字体文件交给 Vite 打包并生成带哈希的静态资源。",[126,2046,2047,2048,2051],{},"组件调用 ",[62,2049,2050],{},"document.fonts.load()","，等待艺术字体真正可用于绘制。",[126,2053,2054],{},"只有字体就绪、组件仍存活、页面可见且请求仍是最新时才开始播放。",[126,2056,2057],{},"字体加载失败时继续使用衬线回退字体，不阻断内容显示。",[126,2059,2060],{},"开启减少动态效果时直接展示最终状态。",[119,2062,2063,2064,2067,2068,2071],{},"每次重播都会增加 ",[62,2065,2066],{},"animationRun","，并把它放进四层的 Vue ",[62,2069,2070],{},"key","。这会让浏览器得到新的动画元素，可靠地从第 0 帧开始，而不是依赖移除 class 后强制读取布局。",[110,2073,2075],{"id":2074},"十四字标在什么时候重播","十四、字标在什么时候重播",[119,2077,2078,2079,2082,2083,2086],{},"全局 ",[62,2080,2081],{},"useWordmarkAnimation()"," 只维护一个数字信号。任何组件调用 ",[62,2084,2085],{},"requestReplay()","，数字加一；字标组件 watch 到变化后重播。这个很小的事件通道避免 RGB 组件直接引用字标 DOM。",[119,2088,2089],{},"当前重播场景包括：",[123,2091,2092,2095,2098,2101,2104,2107,2110],{},[126,2093,2094],{},"首次进入主页并且艺术字体加载完成；",[126,2096,2097],{},"点击主题预设或重置；",[126,2099,2100],{},"RGB 滑杆松手或键盘提交；",[126,2102,2103],{},"从文章、服务、关于页面返回主页，主页组件重新挂载；",[126,2105,2106],{},"浏览器标签页从隐藏变为可见；",[126,2108,2109],{},"窗口重新获得焦点；",[126,2111,2112,2113,2116],{},"浏览器的 ",[62,2114,2115],{},"pageshow","，包括可能来自往返缓存的恢复。",[119,2118,2119,1186,2122,1201,2125,2127],{},[62,2120,2121],{},"visibilitychange",[62,2123,2124],{},"focus",[62,2126,2115],{}," 可能在同一次切回中连续到达。组件使用一个 animation frame 合并激活请求，并设置 200ms 的最小间隔，防止一次切回连续播两三遍。页面变为隐藏时会取消待播放帧、使旧异步请求失效并结束当前播放。",[119,2129,2130],{},"这里还有两个无障碍与容错原则：",[123,2132,2133,2139],{},[126,2134,2135,2138],{},[62,2136,2137],{},"prefers-reduced-motion: reduce"," 时不播放套色，直接显示最终字标；",[126,2140,2141,2142,2145],{},"可读文字仍存在于 ",[62,2143,2144],{},"sr-only"," 中，视觉 mask 不承担标题语义。",[110,2147,2149],{"id":2148},"十五服务矩阵图标与状态点","十五、服务矩阵、图标与状态点",[119,2151,2152],{},"每个服务在 YAML 中有六个字段：名称、说明、链接、状态、图标、是否显示在主页。",[119,2154,2155,2158,2159,1133,2161,2164,2165,2168],{},[62,2156,2157],{},"ServiceIcon.vue"," 把业务名称映射到 Lucide 图标。NAS 使用独立的 ",[62,2160,74],{},[62,2162,2163],{},"HardDrive","，不会复用影音播放图标。未知值虽然有 ",[62,2166,2167],{},"Server"," 回退，但正常内容会在 Schema 阶段被拒绝，因此回退只是组件级防御。",[119,2170,2171],{},"状态共有三种：",[357,2173,2176],{"className":2174,"code":2175,"language":943,"meta":362},[941],"online   在线\nprivate  仅限本人\nsoon     准备中\n",[62,2177,2175],{"__ignoreMap":362},[119,2179,2180,2183,2184,2187,2188,2191],{},[62,2181,2182],{},"ServiceStatusDot.vue"," 把状态写入 ",[62,2185,2186],{},"data-state","，CSS 用属性选择器决定颜色，",[62,2189,2190],{},"title"," 提供文字说明。主页和服务页都复用这个组件，因此 CMS 修改 YAML 状态后，重新构建的两个页面会同步变化，不需要分别维护配色。",[119,2193,2194,2195,2198],{},"状态点表达的是",[129,2196,2197],{},"人工配置的展示状态","，不是实时健康检查。它不会主动请求 Code、Photos、NAS 或 Status，也不会自动从监控系统同步。如果未来需要实时状态，应由独立状态 API 提供可信结果，再决定是在客户端请求还是构建前抓取；不能仅把绿色小点误解为自动监控。",[119,2200,2201,2202,2204,2205,2208,2209,2212,2213,2216],{},"链接为 ",[62,2203,78],{}," 时，主页服务节点转到 ",[62,2206,2207],{},"\u002Fservices","，服务页显示为不可点击行；完整 HTTPS 链接则在新标签页打开，并带 ",[62,2210,2211],{},"noopener noreferrer","。Schema 禁止 ",[62,2214,2215],{},"javascript:"," 和非 HTTPS 外部地址，避免 CMS 字段直接变成危险链接。",[110,2218,2220],{"id":2219},"十六文章列表筛选与搜索","十六、文章列表、筛选与搜索",[119,2222,2223,2226,2227,2230],{},[62,2224,2225],{},"\u002Fwriting"," 在构建阶段只查询 ",[62,2228,2229],{},"hidden = false"," 的文章，并按日期倒序排列。首页最近文章使用同样条件，浏览器端再对已经加载的公开数组做筛选，不需要为每个关键词请求服务器。",[119,2232,2233,2234,2237],{},"分类来自共享常量：随笔、学习、交易、观察。列表额外添加“全部”。搜索会把标题、摘要和标签拼成小写字符串，再做简单的 ",[62,2235,2236],{},"includes"," 匹配。这适合当前文章规模，特点是实现透明、中文可用、没有索引服务；但它不是分词搜索，也不会搜索正文。",[119,2239,2240],{},"文章行显示日期、标题、摘要、分类和箭头。悬浮一篇时：",[123,2242,2243,2246,2249,2252],{},[126,2244,2245],{},"只给当前行增加浅主题色背景；",[126,2247,2248],{},"日期与正文整体平移 8px；",[126,2250,2251],{},"箭头向右上轻移；",[126,2253,2254],{},"其他文章保持完全不变。",[119,2256,2257,2258,1186,2261,2264,2265,2268],{},"标题容器的 grid 列不会在 hover 时改变，所以长标题不会因为悬浮突然获得更小宽度而换行。主页“最近写下”的标题使用 ",[62,2259,2260],{},"minmax(0,1fr)",[62,2262,2263],{},"overflow:hidden","、省略号和 ",[62,2266,2267],{},"white-space:nowrap","，确保固定高度面板不被长标题撑开。",[119,2270,2271,2272,2275],{},"搜索结果区使用 ",[62,2273,2274],{},"aria-live=\"polite\"","，筛选后的篇数和空状态可以被辅助技术感知。",[110,2277,2279],{"id":2278},"十七文章详情与相邻导航","十七、文章详情与相邻导航",[119,2281,2282,2283,2286,2287,2290],{},"动态路由文件是 ",[62,2284,2285],{},"app\u002Fpages\u002Fwriting\u002F[...slug].vue","。它使用当前 ",[62,2288,2289],{},"route.path"," 查询对应文章；找不到时立即抛出带中文说明的 404。",[119,2292,2293,2294,2296],{},"详情查询本身也要求 ",[62,2295,2229],{},"，所以隐藏文章不只是从列表消失，直接访问原 URL 也会得到真正的 404。详情页同时取得按日期倒序排列的全部公开文章，用当前文章的数组位置计算：",[123,2298,2299,2302],{},[126,2300,2301],{},"数组中下一项是时间上更旧的“上一篇”；",[126,2303,2304],{},"数组中上一项是时间上更新的“下一篇”。",[119,2306,2307,2308,2310],{},"页面展示分类、发布日期、可选的更新日期、标题、摘要、标签与正文。正文由 Nuxt Content 的 ",[62,2309,1329],{}," 生成。SEO 标题使用全站模板变成“文章标题 · Quilibra”，描述来自 frontmatter，Open Graph 类型设为 article。",[119,2312,2313,2314,2317],{},"正文样式集中在 ",[62,2315,2316],{},".article-prose","：限制阅读宽度、提高行高、分级处理标题、列表、引用、代码、链接和图片。文章页面可以自由使用 Markdown，不需要为每篇文章编写 Vue 模板。",[110,2319,2321],{"id":2320},"十八文章-frontmatter-合约","十八、文章 frontmatter 合约",[119,2323,2324],{},"一篇新文章最小结构如下：",[357,2326,2330],{"className":2327,"code":2328,"language":2329,"meta":362,"style":362},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 清楚的文章标题\ndescription: 一句话说明文章实际记录了什么。\ndate: 2026-07-26\nhidden: false\ncategory: 学习\ntags:\n  - Nuxt\n---\n","yaml",[62,2331,2332,2337,2347,2357,2367,2377,2388,2397,2406],{"__ignoreMap":362},[366,2333,2334],{"class":368,"line":369},[366,2335,2336],{"class":1562},"---\n",[366,2338,2339,2341,2344],{"class":368,"line":537},[366,2340,2190],{"class":1558},[366,2342,2343],{"class":1554},": ",[366,2345,2346],{"class":1569},"清楚的文章标题\n",[366,2348,2349,2352,2354],{"class":368,"line":35},[366,2350,2351],{"class":1558},"description",[366,2353,2343],{"class":1554},[366,2355,2356],{"class":1569},"一句话说明文章实际记录了什么。\n",[366,2358,2359,2362,2364],{"class":368,"line":1759},[366,2360,2361],{"class":1558},"date",[366,2363,2343],{"class":1554},[366,2365,2366],{"class":1746},"2026-07-26\n",[366,2368,2369,2372,2374],{"class":368,"line":1769},[366,2370,2371],{"class":1558},"hidden",[366,2373,2343],{"class":1554},[366,2375,2376],{"class":1746},"false\n",[366,2378,2380,2383,2385],{"class":368,"line":2379},6,[366,2381,2382],{"class":1558},"category",[366,2384,2343],{"class":1554},[366,2386,2387],{"class":1569},"学习\n",[366,2389,2391,2394],{"class":368,"line":2390},7,[366,2392,2393],{"class":1558},"tags",[366,2395,2396],{"class":1554},":\n",[366,2398,2400,2403],{"class":368,"line":2399},8,[366,2401,2402],{"class":1554},"  - ",[366,2404,2405],{"class":1569},"Nuxt\n",[366,2407,2409],{"class":368,"line":2408},9,[366,2410,2336],{"class":1562},[119,2412,2413],{},"文件名必须是：",[357,2415,2418],{"className":2416,"code":2417,"language":943,"meta":362},[941],"YYYY-MM-DD-english-kebab.md\n",[62,2419,2417],{"__ignoreMap":362},[119,2421,2422,2423,1186,2425,1186,2427,1186,2429,1186,2432,1186,2435,1186,2437,1186,2439,238,2441,2443,2444,2446],{},"允许字段只有 ",[62,2424,2190],{},[62,2426,2351],{},[62,2428,2361],{},[62,2430,2431],{},"updated",[62,2433,2434],{},"slug",[62,2436,2371],{},[62,2438,2382],{},[62,2440,2393],{},[62,2442,2371],{}," 只能是布尔值，省略时按 ",[62,2445,1533],{}," 处理。发布新文章时：",[123,2448,2449,2454,2460,2465],{},[126,2450,2451,2453],{},[62,2452,2361],{}," 必须是中国时区当天；",[126,2455,2456,2457,2459],{},"文件名日期必须与 ",[62,2458,2361],{}," 一致；",[126,2461,2462,2463,1224],{},"不能包含 ",[62,2464,2431],{},[126,2466,2467,2469],{},[62,2468,2434],{}," 如果存在，必须与文件名后半段一致。",[119,2471,2472,2473,2475,2476,2478,2479,2482],{},"修订文章时保留原文件名和 ",[62,2474,2361],{},"，把 ",[62,2477,2431],{}," 设置为中国时区当天。writer 默认保留现有隐藏状态，只有修订内容明确给出 ",[62,2480,2481],{},"hidden: false"," 才会恢复。正文结构是自由的，但不能为空。若使用二级标题“来源与延伸阅读”，该节必须至少有一个 HTTP 或 HTTPS 链接。",[119,2484,2485,2488,2489,2492],{},[62,2486,2487],{},"scripts\u002Fcontent\u002Fvalidate.ts"," 使用 ",[62,2490,2491],{},"gray-matter"," 和 YAML 解析 frontmatter，再用 Markdown AST 检查正文与来源节。使用结构化解析而不是正则扫描全文，可以区分标题、链接与普通文本，并给出确定的错误。",[110,2494,2496],{"id":2495},"十九sveltia-cms-到底是什么","十九、Sveltia CMS 到底是什么",[119,2498,2499,2502,2503,2505],{},[62,2500,2501],{},"\u002Fadmin\u002Findex.html"," 只加载 Sveltia CMS 的前端脚本，",[62,2504,1474],{}," 描述后台字段。它不是另一个网站后端，也不存文章副本。",[119,2507,2508],{},"后台能维护：",[123,2510,2511,2514,2517,2520,2523,2526],{},[126,2512,2513],{},"站点、首页与栏目文案；",[126,2515,2516],{},"默认强调色和主页模块开关；",[126,2518,2519],{},"服务的增删、顺序、URL、状态、图标和主页显示；",[126,2521,2522],{},"文章的新建、搜索、筛选、修改和删除；",[126,2524,2525],{},"文章的隐藏和恢复；",[126,2527,2528,2530],{},[62,2529,1204],{}," 中的文章资源。",[119,2532,2533],{},"CMS 有两种工作方式：",[277,2535,2536,2539],{},[126,2537,2538],{},"本地仓库模式：Chromium 通过 File System Access API 直接编辑本机选择的仓库。",[126,2540,2541],{},"GitHub 模式：用户经 GitHub 登录后，CMS 根据配置向私有仓库提交内容。",[119,2543,2544,2545,2547],{},"无论哪种模式，“保存”都只意味着源文件或远程仓库出现改动。它不会直接覆盖某个静态目录。远程 CMS 当前固定写入私人仓库的 ",[62,2546,915],{}," 分支，完整链路是：",[357,2549,2552],{"className":2550,"code":2551,"language":943,"meta":362},[941],"dev.quilibra.cn\u002Fadmin\n        |\n        | GitHub OAuth 保存\n        v\nGitHub staging\n        |\n        | GitHub push Webhook，HMAC-SHA256 签名\n        v\nVPS 事件队列 -> staging 同步 -> 快速或完整构建 -> staging release -> 127.0.0.1:3100\n        |\n        | 人工确认测试页面后 npm run promote\n        v\nGitHub master\n        |\n        | GitHub push Webhook，HMAC-SHA256 签名\n        v\nVPS 事件队列 -> master 同步 -> 快速或完整构建 -> production release -> 127.0.0.1:3000\n",[62,2553,2551],{"__ignoreMap":362},[119,2555,2556,2557,2560],{},"因此，在 CMS 打开“隐藏文章”并保存，只会先让测试站列表和原 URL 发生变化，正式站保持不动。确认后执行 ",[62,2558,2559],{},"npm run promote","，生产才接收同一个内容状态。这是分支、构建目录、release 和进程都真实隔离的预发布环境，不是给同一份生产内容换了一个域名。",[119,2562,2563,2564,2567],{},"GitHub OAuth 只控制“谁能通过 CMS 读写私人仓库”。本项目不使用 GitHub Actions 部署，也不开放公网 SSH；GitHub 只向现有 HTTPS 精确路径发送带签名的 push 通知，VPS 收到后再通过 Sing-box HTTP 代理主动拉取对应分支。Webhook secret 只在 VPS 和 GitHub 仓库设置中保存。私人仓库存放网站源码、文章、公开图片和不含密钥的配置；OAuth secret、访问令牌、Webhook secret、部署私钥、OpenClaw 会话、cookie 和 ",[62,2565,2566],{},".env"," 不进入仓库。",[119,2569,2570,2571,2574,2575,2578,2579,2581],{},"远程后台统一通过 ",[62,2572,2573],{},"https:\u002F\u002Fdev.quilibra.cn\u002Fadmin\u002Findex.html"," 使用。Nginx 在 TLS 之外再加一层 HTTP Basic Auth，并发送 ",[62,2576,2577],{},"noindex","；GitHub OAuth 负责仓库权限，Basic Auth 负责挡住后台入口，两者职责不同。后台路径和 ",[62,2580,2577],{}," 本身都不是访问控制。",[110,2583,2585],{"id":2584},"二十受限-writer-为什么存在","二十、受限 writer 为什么存在",[119,2587,2588,2589,2592],{},"普通编辑器可以修改仓库中的任何文件，但自动化发布工具不应该拥有同样宽的写入自由。",[62,2590,2591],{},"scripts\u002Fcontent\u002Fwrite.ts"," 把 OpenClaw 的写入范围压缩为三个明确操作：create、update、delete。",[119,2594,2595],{},"新建流程只接受草稿目录中的一个普通 Markdown 文件名和一个无日期 slug。writer 会检查：",[123,2597,2598,2601,2604,2607,2610,2613,2616],{},[126,2599,2600],{},"草稿名不能包含目录或路径穿越；",[126,2602,2603],{},"草稿必须是普通文件，不能是符号链接；",[126,2605,2606],{},"草稿不超过 1 MiB；",[126,2608,2609],{},"slug 必须是小写英文、数字和连字符；",[126,2611,2612],{},"frontmatter 与正文必须通过完整校验；",[126,2614,2615],{},"日期必须是中国时区当天；",[126,2617,2618],{},"目标文章不能已经存在。",[119,2620,2621,2622,2625,2626,2629,2630,2633,2634,2637],{},"写入时先以 ",[62,2623,2624],{},"wx"," 模式建立权限为 ",[62,2627,2628],{},"0640"," 的随机临时文件并 ",[62,2631,2632],{},"fsync","，再用硬链接创建最终文件，利用文件系统的 ",[62,2635,2636],{},"EEXIST"," 保证不会覆盖已有新文章。更新时先验证现有文件和发布日期，再用 rename 原子替换。删除只接受精确的完整 dated slug，并拒绝目录、符号链接和不存在的目标。",[119,2639,2640],{},"这不是为了让日常写作变复杂，而是为了让“从聊天自动发布”仍有可审计的硬边界。Skill 中的文字规则可能被模型误解，writer 的路径、日期和文件类型检查则由操作系统与代码强制执行。",[110,2642,2644],{"id":2643},"二十一openclaw-发布-skill-的实际状态机","二十一、OpenClaw 发布 Skill 的实际状态机",[119,2646,2647,2648,2651],{},"仓库只维护一个 ",[62,2649,2650],{},"personal-site-publisher"," Skill。它先读取文章合约，再按以下过程工作：",[357,2653,2656],{"className":2654,"code":2655,"language":943,"meta":362},[941],"链接或素材\n   |\n   v\n确定标题、摘要、日期、slug、分类、标签\n   |\n   v\n在私聊中返回完整草稿，不写仓库\n   |\n   +-- 修改：... --> 修改内存草稿\n   +-- 放弃 ------> 清除内存草稿\n   +-- 发布 ------> 保存到受限草稿目录\n                         |\n                         v\n                   content:write\n                         |\n                         v\n                  npm run publish\n                         |\n                         v\n              返回文章 URL、commit、release\n",[62,2657,2655],{"__ignoreMap":362},[119,2659,2660,2663,2664,238],{},[62,2661,2662],{},"直接发布"," 只跳过人工预览，不跳过 writer、内容校验和静态生成。更新文章必须保留 dated URL；删除必须先展示目标信息，再收到完全匹配的 ",[62,2665,2666],{},"确认删除 \u003Cdated-slug>",[119,2668,2669,2670,1186,2673,1949,2676,2679],{},"发布 Skill 还规定：只接受所有者私聊中的发布命令；把网页、对话和搜索结果视为不可信内容；不把 API key、cookie、账号信息、私人对话细节和精确私人资产写入文章；不能用任意编辑器、",[62,2671,2672],{},"rm",[62,2674,2675],{},"git add",[62,2677,2678],{},"git commit"," 绕过 writer 和 release 脚本。",[110,2681,2683],{"id":2682},"二十二本地发布自动同步与环境晋级","二十二、本地发布、自动同步与环境晋级",[119,2685,2686],{},"当前有五个相关入口，名字接近但权限完全不同。",[115,2688,2690],{"id":2689},"npm-run-publish",[62,2691,2692],{},"npm run publish",[119,2694,2695],{},"用于受信本地流程中尚未提交的内容改动，例如 OpenClaw Skill。它只允许以下路径：",[357,2697,2700],{"className":2698,"code":2699,"language":943,"meta":362},[941],"content\u002Fsettings\u002Fsite.yml\ncontent\u002Fsettings\u002Fhome.yml\ncontent\u002Fsettings\u002Fwriting.yml\ncontent\u002Fsettings\u002Fservices.yml\ncontent\u002Fsettings\u002Fabout.yml\ncontent\u002Fwriting\u002F**\npublic\u002Fuploads\u002F**\n",[62,2701,2699],{"__ignoreMap":362},[119,2703,2704],{},"它验证内容、生成静态站、自动创建本地 Git commit、复制 release 并切换本地默认生产目录。若工作区混入 Vue、CSS、脚本、CMS 配置或文档改动，会拒绝执行。它是受限本地发布入口，不是远程 CMS 的预发布流程。",[115,2706,2708],{"id":2707},"npm-run-deploy",[62,2709,2710],{},"npm run deploy",[119,2712,2713],{},"用于已经提交的代码版本。它要求工作区完全干净，不创建 commit，并依次运行：",[357,2715,2718],{"className":2716,"code":2717,"language":943,"meta":362},[941],"类型检查\n内容与 writer 单元测试\nrelease 单元测试\n静态生成\n浏览器 E2E\n创建 release\n原子切换 current\n清理过旧 release\n",[62,2719,2717],{"__ignoreMap":362},[115,2721,2723],{"id":2722},"npm-run-sync",[62,2724,2725],{},"npm run sync",[119,2727,2728,2731,2732,2734,2735,2737],{},[62,2729,2730],{},"scripts\u002Fsync.ts"," 是两个 systemd oneshot 服务共用的自动同步器。环境变量决定它跟踪 ",[62,2733,923],{}," 还是 ",[62,2736,915],{},"、使用哪一份独立仓库、把 release 写到哪个数据根目录。每轮会：",[277,2739,2740,2743,2746,2756,2761,2767],{},[126,2741,2742],{},"从 Sing-box 配置读取带认证的本机 HTTP 代理；",[126,2744,2745],{},"重试 GitHub fetch，只接受能够快进的远端历史；",[126,2747,2748,2749,2752,2753,1224],{},"根据 ",[62,2750,2751],{},"package-lock.json"," 哈希决定是否运行 ",[62,2754,2755],{},"npm ci",[126,2757,2758,2759,1224],{},"比较上次已部署提交与目标提交：只有受管内容变化时执行快速内容部署，否则执行完整 ",[62,2760,2710],{},[126,2762,2763,2764,1224],{},"只有部署成功才原子写入 ",[62,2765,2766],{},"deployed-commit",[126,2768,2769],{},"失败时保留旧 release，由持久化 Webhook 事件延迟重试。",[119,2771,2772,2773,2776],{},"纯文章、站点设置和上传资源变化仍会执行内容校验与静态生成，但跳过未变化代码的类型检查、单元测试和 E2E。生产和测试同步服务使用同一个 ",[62,2774,2775],{},"flock"," 文件串行执行，防止 2 GB 内存的 VPS 同时跑两份 Nuxt 构建和 Playwright。",[115,2778,2780],{"id":2779},"npm-run-webhook",[62,2781,2782],{},"npm run webhook",[119,2784,2785,2788,2789,2792,2793,2796,2797,2800,2801,2804,2805,238],{},[62,2786,2787],{},"scripts\u002Fwebhook.ts"," 只监听 ",[62,2790,2791],{},"127.0.0.1:3110","，Nginx 把 ",[62,2794,2795],{},"https:\u002F\u002Fquilibra.cn\u002F_hooks\u002Fgithub"," 的精确 POST 路由转给它。接收器先验证 HMAC-SHA256 签名、仓库名、push 类型和 ",[62,2798,2799],{},"staging\u002Fmaster"," 分支，再把事件以权限 ",[62,2802,2803],{},"0600"," 写入用户状态目录并返回 ",[62,2806,2807],{},"202",[119,2809,2810],{},"每个 GitHub delivery 都有独立队列文件。若构建期间出现第二次 push，第一轮完成后会再启动一轮同步；进程重启时也会恢复未完成事件。失败使用延迟重试，不需要周期性轮询 GitHub。Webhook 请求只能映射到两个固定 systemd unit，不能携带或执行任意命令。",[115,2812,2814],{"id":2813},"npm-run-promote",[62,2815,2559],{},[119,2817,2818,2819,2821,2822,2824],{},"这是远程 CMS 从测试进入生产的唯一显式关口。它要求本地 ",[62,2820,923],{}," 干净且等于最新远端，确认远端 ",[62,2823,915],{}," 的精确提交已经成功部署到测试站，并拒绝 staging 中的 Vue、CSS、脚本、文档或 CMS 配置改动。允许晋级的只有：",[357,2826,2828],{"className":2827,"code":2699,"language":943,"meta":362},[941],[62,2829,2699],{"__ignoreMap":362},[119,2831,2832,2833,2836,2837,1201,2839,2841],{},"检查通过后，脚本在临时 Git worktree 中把 staging 合并到最新 master，再用一次 ",[62,2834,2835],{},"git push --atomic"," 让远端 ",[62,2838,923],{},[62,2840,915],{}," 同时指向同一个合并提交。若任一引用不能更新，两边都不更新。GitHub 随后发送 master Webhook 并触发正式部署。",[119,2843,2844,2845,2848,2849,2851,2852,2855,2856,2858,2859,2861],{},"因此，修改本文这样的文章有两种合法路径：本地受信流程可用 ",[62,2846,2847],{},"publish","；远程 CMS 则保存到 staging、检查测试站、再运行 ",[62,2850,919],{},"。修改 Vue\u002FCSS 并已经提交后使用 ",[62,2853,2854],{},"deploy"," 或由 master 同步器部署。不要用 ",[62,2857,2847],{}," 偷渡代码，也不要让 ",[62,2860,2854],{}," 带着未提交文件上线。",[110,2863,2865],{"id":2864},"二十三静态-release-如何做到切换时不中断","二十三、静态 release 如何做到切换时不中断",[119,2867,2868,2871],{},[62,2869,2870],{},"scripts\u002Frelease.ts"," 用 UTC 时间生成 release 名称，例如：",[357,2873,2876],{"className":2874,"code":2875,"language":943,"meta":362},[941],"20260726T144914-134Z\n",[62,2877,2875],{"__ignoreMap":362},[119,2879,2880,2881,2883,2884,2887,2888,2890],{},"生成结束后，把 ",[62,2882,970],{}," 递归复制到 ",[62,2885,2886],{},"releases\u002F\u003Cid>","。激活时不是先删 ",[62,2889,974],{}," 再新建，而是：",[277,2892,2893,2896,2899],{},[126,2894,2895],{},"创建一个带进程号的临时符号链接；",[126,2897,2898],{},"让临时链接指向新 release；",[126,2900,2901,2902,2905,2906,238],{},"用 ",[62,2903,2904],{},"rename"," 把临时链接原子替换成 ",[62,2907,974],{},[119,2909,2910,2911,2913,2914,2916],{},"对访问者来说，某个请求要么读取旧 release，要么读取新 release，不会遇到 ",[62,2912,974],{}," 暂时不存在的中间状态。静态 Node 进程也不需要重启，因为它每次请求都从 ",[62,2915,974],{}," 路径解析文件。",[119,2918,2919,2922,2923,2926,2927,2930,2931,1201,2934,238],{},[62,2920,2921],{},"PERSONAL_SITE_DATA_ROOT"," 决定 release 根目录。生产不设置时使用 ",[62,2924,2925],{},"~\u002F.local\u002Fshare\u002Fpersonal-site","；测试同步服务显式设置为 ",[62,2928,2929],{},"~\u002F.local\u002Fshare\u002Fpersonal-site-staging","。因此相同的 release 代码可以复用，但两个环境不会碰到对方的 ",[62,2932,2933],{},"releases\u002F",[62,2935,974],{},[119,2937,2938],{},"脚本默认保留最近五个 release，并额外保证当前激活版本不被删掉。由于当前版本有可能是手工回滚到的较旧目录，保留“当前版本”比单纯保留时间最新的五个更重要。",[119,2940,2941,2942,2944],{},"回滚只调用同一个原子激活函数，把 ",[62,2943,974],{}," 指回已存在的 release。它不修改 Git、不重新构建、不删除较新的版本，也不重启生产服务。",[110,2946,2948],{"id":2947},"二十四静态服务器nginx-与-https","二十四、静态服务器、Nginx 与 HTTPS",[119,2950,2951,2954,2955,1186,2958,1186,2961,1201,2964,2967,2968,2970,2971,1186,2974,1186,2977,2980,2981,2984],{},[62,2952,2953],{},"scripts\u002Fstatic-server.mjs"," 使用 Node 自带的 ",[62,2956,2957],{},"http",[62,2959,2960],{},"fs",[62,2962,2963],{},"stream",[62,2965,2966],{},"zlib","。监听地址、端口和 ",[62,2969,974],{}," 路径分别由 ",[62,2972,2973],{},"PERSONAL_SITE_HOST",[62,2975,2976],{},"PERSONAL_SITE_PORT",[62,2978,2979],{},"PERSONAL_SITE_ROOT"," 控制。两个 systemd unit 都把 host 设为 ",[62,2982,2983],{},"127.0.0.1","，所以 3000 和 3100 不直接接受公网连接。它没有框架中间件，职责非常窄：",[123,2986,2987,2990,3003,3009,3016,3019,3022,3025,3028,3031,3038,3044,3047],{},[126,2988,2989],{},"只接受 GET 和 HEAD，其他方法返回 405；",[126,2991,2992,2993,1133,2996,2999,3000,3002],{},"解码 URL 后用 ",[62,2994,2995],{},"resolve",[62,2997,2998],{},"relative"," 检查目标仍在 ",[62,3001,974],{}," 内，阻止目录穿越；",[126,3004,3005,3006,1224],{},"请求目录时返回该目录的 ",[62,3007,3008],{},"index.html",[126,3010,3011,3012,3015],{},"找不到文件时返回预生成的 ",[62,3013,3014],{},"404.html"," 和 404 状态；",[126,3017,3018],{},"根据扩展名发送正确的 Content-Type；",[126,3020,3021],{},"对大于等于 1 KiB 的文本资源协商 Brotli 或 gzip；",[126,3023,3024],{},"Brotli 使用质量 4，gzip 使用级别 6，在 CPU 与体积间取中间值；",[126,3026,3027],{},"使用读取流和 pipeline，不把大文件一次性读进内存；",[126,3029,3030],{},"根据文件大小和修改时间生成弱 ETag；",[126,3032,3033,3034,3037],{},"命中 ",[62,3035,3036],{},"If-None-Match"," 时返回 304；",[126,3039,3040,3043],{},[62,3041,3042],{},"_nuxt\u002F"," 哈希资源缓存一年并标记 immutable；",[126,3045,3046],{},"HTML、图片等非哈希路径使用 no-cache，确保 release 切换后会重新验证；",[126,3048,3049],{},"收到 SIGINT 或 SIGTERM 时停止接受新连接并正常退出。",[119,3051,3052,3053,3055],{},"如果 ",[62,3054,974],{}," 不可用，服务器返回 503，而不是把文件系统错误暴露给访问者。若响应流已经开始后出错，则销毁连接，避免发送一半文件后再拼接错误文本。",[119,3057,3058],{},"它不会处理 TLS、OAuth 或反向代理。公网入口由 Nginx 负责：",[357,3060,3063],{"className":3061,"code":3062,"language":943,"meta":362},[941],"quilibra.cn \u002F www.quilibra.cn -> HTTPS -> 127.0.0.1:3000\ndev.quilibra.cn               -> HTTPS + Basic Auth -> 127.0.0.1:3100\n公网 IP或未知 Host             -> 默认站点拒绝\n",[62,3064,3062],{"__ignoreMap":362},[119,3066,3067,3068,3071,3072,3075],{},"根域名和 ",[62,3069,3070],{},"www"," 共用正式站，证书由 Certbot 申请并续期；测试子域名使用自己的证书和密码文件。Nginx 把真实 Host、客户端地址和转发协议传给上游，测试站额外发送 ",[62,3073,3074],{},"X-Robots-Tag: noindex, nofollow, noarchive","。腾讯云安全组只需要开放 80 和 443，3000、3100 不对公网放行。",[119,3077,3078,3079,3082,3083,3086],{},"页脚备案号来自全站设置 ",[62,3080,3081],{},"content\u002Fsettings\u002Fsite.yml","，当前 ICP 号链接到 ",[62,3084,3085],{},"https:\u002F\u002Fbeian.miit.gov.cn\u002F","。备案展示、DNS、TLS 证书和反向代理是四个不同层次：备案号不会自动配置 HTTPS，证书也不会自动创建 DNS 记录。",[110,3088,3090],{"id":3089},"二十五300031003101-分别是什么","二十五、3000、3100、3101 分别是什么",[149,3092,3093,3106],{},[152,3094,3095],{},[155,3096,3097,3100,3103],{},[158,3098,3099],{},"端口",[158,3101,3102],{},"用途",[158,3104,3105],{},"生命周期",[165,3107,3108,3119,3131],{},[155,3109,3110,3113,3116],{},[170,3111,3112],{},"3000",[170,3114,3115],{},"当前生产静态 release",[170,3117,3118],{},"systemd 长期运行",[155,3120,3121,3124,3129],{},[170,3122,3123],{},"3100",[170,3125,3126,3128],{},[62,3127,915],{}," 当前静态 release",[170,3130,3118],{},[155,3132,3133,3136,3139],{},[170,3134,3135],{},"3101",[170,3137,3138],{},"Playwright 隔离测试服务",[170,3140,3141],{},"测试自动启动并结束",[119,3143,3144,3145,3148,3149,3152],{},"3100 过去曾运行当前工作区的 Nuxt Dev，容易受首次编译、热更新和 Nuxt Content 缓存损坏影响。现在它与 3000 一样只运行静态服务器，从独立 staging release 读取文件，空闲常驻约几十 MB，并由 ",[62,3146,3147],{},"https:\u002F\u002Fdev.quilibra.cn"," 提供远程预览。需要真正的热更新时，在开发机或 VPS 的另一个空闲端口临时运行 ",[62,3150,3151],{},"npm run dev","，不要覆盖常驻 3100。",[119,3154,3155,3156,3158],{},"3101 使用独立内容 fixture 和 ",[62,3157,1266],{},"，不能拿它当日常预览端口。生产 3000、预发布 3100 和测试 3101 可以同时存在，因为 release、构建目录和端口都隔离。",[110,3160,3162],{"id":3161},"二十六systemd-用户服务","二十六、systemd 用户服务",[119,3164,3165],{},"当前有五个相关 unit：",[357,3167,3170],{"className":3168,"code":3169,"language":943,"meta":362},[941],"personal-site.service                 3000 生产静态服务\npersonal-site-dev.service             3100 staging 静态服务\npersonal-site-webhook.service         3110 签名 Webhook 接收与事件排队\npersonal-site-sync.service            master 单次同步与部署\npersonal-site-staging-sync.service    staging 单次同步与部署\n",[62,3171,3169],{"__ignoreMap":362},[119,3173,3174],{},"两个静态服务的关键设置包括：",[123,3176,3177,3183,3189,3195,3201,3210],{},[126,3178,3179,3182],{},[62,3180,3181],{},"Type=simple","：Node 进程本身就是主进程；",[126,3184,3185,3188],{},[62,3186,3187],{},"After\u002FWants=network-online.target","：网络就绪后启动；",[126,3190,3191,3194],{},[62,3192,3193],{},"Restart=on-failure"," 与 5 秒延迟：异常退出后自动恢复；",[126,3196,3197,3200],{},[62,3198,3199],{},"KillSignal=SIGINT","：与静态服务器的优雅关闭逻辑对应；",[126,3202,3203,1186,3206,3209],{},[62,3204,3205],{},"NoNewPrivileges=true",[62,3207,3208],{},"PrivateTmp=true","：缩小运行时权限；",[126,3211,3212,3213,3216],{},"安装到用户级 ",[62,3214,3215],{},"default.target","，不要求整个服务以 root 运行。",[119,3218,3219,3220,3222,3223,3226,3227,1133,3230,3232,3233,3236],{},"Webhook 服务是 ",[62,3221,3181],{}," 常驻轻量进程，只监听回环地址；两个同步服务仍是 ",[62,3224,3225],{},"Type=oneshot","，仅在收到对应分支的 push 后运行。生产和测试分别拥有独立 ",[62,3228,3229],{},"sync-repo",[62,3231,2766],{},"，但用同一个 ",[62,3234,3235],{},"\u002Fhome\u002Fuser\u002F.local\u002Fstate\u002Fpersonal-site-deploy.lock"," 串行构建。仓库没有周期性 GitHub timer；有事件时才 fetch，依赖锁变化时才安装依赖，纯内容提交使用快速构建。",[119,3238,3239,3240,3243,3244,3247],{},"unit 当前把 Node 可执行文件写成 NVM 下的明确路径。升级 Node 或切换 NVM 版本后，shell 中的 ",[62,3241,3242],{},"node"," 可能已经变化，但 systemd 仍引用旧路径，这是需要主动检查的运维陷阱。修改 unit 后必须 ",[62,3245,3246],{},"daemon-reload","，而普通文章或页面 release 不需要重启静态服务。",[119,3249,3250,3251,3254,3255,3258],{},"用户级服务要在 VPS 重启且用户未登录时自动启动，需要为运行网站的 Linux 用户启用 linger。管理它时应使用相同用户的 ",[62,3252,3253],{},"systemctl --user","，不应使用 ",[62,3256,3257],{},"sudo systemctl --user"," 去连接 root 的用户总线。",[110,3260,3262],{"id":3261},"二十七测试体系究竟覆盖什么","二十七、测试体系究竟覆盖什么",[119,3264,3265],{},"测试按风险边界分成三组。",[115,3267,3269],{"id":3268},"内容和-writer-单元测试","内容和 writer 单元测试",[119,3271,3272,3275],{},[62,3273,3274],{},"test:content"," 检查：",[123,3277,3278,3281,3284,3287,3290,3295,3298,3301,3304],{},[126,3279,3280],{},"合法文章和四种分类可以通过；",[126,3282,3283],{},"非法日期、分类、文件名、重复标签和空正文会失败；",[126,3285,3286],{},"来源节必须包含公共 HTTP(S) 链接；",[126,3288,3289],{},"CMS 可选 slug 必须与文件名一致；",[126,3291,3292,3294],{},[62,3293,2371],{}," 只能是布尔值，writer 更新时默认保留已有隐藏状态；",[126,3296,3297],{},"站点 YAML 满足 Schema；",[126,3299,3300],{},"writer 拒绝覆盖、穿越、符号链接和错误修订日期；",[126,3302,3303],{},"更新保留发布日期；",[126,3305,3306],{},"删除只接受精确存在的 dated slug。",[115,3308,3310],{"id":3309},"release-单元测试","release 单元测试",[119,3312,3313,3316],{},[62,3314,3315],{},"test:release"," 在临时目录中检查：",[123,3318,3319,3324,3327,3330,3333,3336,3339,3342,3345,3348],{},[126,3320,3321,3323],{},[62,3322,974],{}," 能原子切换；",[126,3325,3326],{},"清理历史版本时保留激活 release；",[126,3328,3329],{},"release 名称不能路径穿越；",[126,3331,3332],{},"受管内容路径不会扩大到应用代码；",[126,3334,3335],{},"自动同步失败后不写成功状态，后续重试仍会部署；",[126,3337,3338],{},"Webhook 拒绝无效签名、其他仓库和其他分支；",[126,3340,3341],{},"构建期间到达的第二个 push 会保留并追加同步；",[126,3343,3344],{},"纯内容提交走快速构建，代码提交走完整构建；",[126,3346,3347],{},"staging 未成功部署时不能晋级，晋级后 master 与 staging 原子对齐；",[126,3349,3350],{},"子命令失败时错误信息保留退出码。",[115,3352,3354],{"id":3353},"playwright-浏览器测试","Playwright 浏览器测试",[119,3356,3357,3360],{},[62,3358,3359],{},"test:e2e"," 在 Chromium 中检查：",[123,3362,3363,3366,3369,3372,3375,3378,3381,3384,3387,3390,3393,3396],{},[126,3364,3365],{},"首页成功水合且没有控制台错误；",[126,3367,3368],{},"动态 favicon、theme-color 和 RGB 输出同步；",[126,3370,3371],{},"自定义主题在纯黑纯白下仍可读；",[126,3373,3374],{},"旧黄色缓存会迁移；",[126,3376,3377],{},"mask 没加载完时字标不偷跑，加载后正常播放；",[126,3379,3380],{},"标签页激活、窗口 focus、换色、滑杆松手和返回主页会重播；",[126,3382,3383],{},"拖动期间不会反复播放；",[126,3385,3386],{},"服务状态、NAS 图标、最近文章、文章详情和更新日期正确；",[126,3388,3389],{},"隐藏文章不出现在公开查询中，原 URL 返回 404；",[126,3391,3392],{},"文章悬浮只影响当前行，长标题宽度不跳变；",[126,3394,3395],{},"手机页面无横向溢出，RGB 在小屏隐藏；",[126,3397,3398],{},"多种桌面尺寸下关键模块互不重叠。",[119,3400,3401],{},"Playwright 配置允许一次重试，等待时间 10 秒，临时 Nuxt 服务启动最长等待 120 秒。响应式几何测试启用 reduced motion，避免入场动画中的瞬时位置影响布局判断。",[119,3403,3404],{},"测试不是越多越好。这里保留的是曾经真实出错、或者一旦回归就很难靠类型系统发现的行为；字体文件数量、每个 CSS 颜色和每一段正文不需要分别写测试。",[110,3406,3408],{"id":3407},"二十八当前性能画像与做过的优化","二十八、当前性能画像与做过的优化",[119,3410,3411],{},"在当前版本的一次本机测量中：",[123,3413,3414,3417,3420,3423,3426],{},[126,3415,3416],{},"主 CSS 约 31.1 KB，gzip 后约 6.9 KB；",[126,3418,3419],{},"外部或本地字体文件由 98 个减少到 0；",[126,3421,3422],{},"首页一次传输约 252 KB；",[126,3424,3425],{},"模拟 4G 和 4 倍 CPU 降速时，LCP 约 0.75 秒，完整 load 约 1.37 秒；",[126,3427,3428],{},"CLS 为 0。",[119,3430,3431,3432,3435],{},"VPS 空闲状态下，生产与测试两个 Node 静态服务合计约 50 MB；Webhook 接收器也是单一轻量 Node 进程，不执行构建。整机仍有约 1.1 GB 可用内存。完整 Nuxt + Playwright 部署的峰值接近 1 GB，但两个同步器通过共享锁串行执行；内容快速构建不启动 Playwright。生产、测试和开发三份仓库合计约 1.5 GB，主要空间来自各自的 ",[62,3433,3434],{},"node_modules","；这是用磁盘换取工作区、缓存和发布故障互不污染。",[119,3437,3438],{},"这些数字是环境相关快照，不是永久承诺。浏览器缓存、网络、CPU、文章数量和依赖升级都会改变结果。",[119,3440,3441],{},"当前主要性能措施包括：",[123,3443,3444,3447,3450,3453,3460,3465,3468,3471,3474,3477,3480],{},[126,3445,3446],{},"静态预渲染，HTML 首次响应包含页面内容；",[126,3448,3449],{},"移除大批字体资源，改用系统字体；",[126,3451,3452],{},"只预加载首屏真正依赖的 30 KB 字标 mask；",[126,3454,3455,3456,3459],{},"字标等待 ",[62,3457,3458],{},"decode()","，避免资源未准备好时动画丢失；",[126,3461,3462,3464],{},[62,3463,1662],{}," 合并高频指针更新；",[126,3466,3467],{},"缓存元素边界，避免每次 pointermove 读取布局；",[126,3469,3470],{},"连续调色只改颜色，松手才重播动画；",[126,3472,3473],{},"用 transform\u002Fopacity 处理大多数动效；",[126,3475,3476],{},"生产服务器流式响应并支持 Brotli、gzip、ETag 和长期哈希缓存；",[126,3478,3479],{},"固定组件尺寸，避免动态文本造成布局偏移；",[126,3481,3482],{},"分离本地开发、生产同步、测试同步和 E2E 的工作目录与 Nuxt Content 数据库。",[119,3484,3485,3487],{},[62,3486,970],{}," 整体约 5.4 MB，其中包括 Nuxt Content 的客户端 SQLite\u002FWASM 相关资源；这不等于每个访客首屏都会下载整个目录。分析性能时应看浏览器实际请求和传输体积，不应把产物目录大小直接当作首页流量。",[110,3489,3491],{"id":3490},"二十九为什么首次交互偶尔会过一会儿才正常","二十九、为什么首次交互偶尔会“过一会儿才正常”",[119,3493,3494],{},"这个问题曾同时表现为字标不播放和 RGB 滑杆暂时失灵，根因方向不是滑杆 CSS 本身，而是客户端水合尚未完成或 Nuxt Dev 仍在首次编译。静态 HTML 可以先显示，但 Vue 事件只有在 JavaScript 加载、执行并成功水合后才生效。",[119,3496,3497,3498,3501],{},"判断方法是观察 ",[62,3499,3500],{},".site-shell[data-hydrated=\"true\"]","。如果页面已经显示但该属性迟迟没有变成 true，应检查：",[123,3503,3504,3507,3510,3513,3516],{},[126,3505,3506],{},"浏览器控制台是否有 JavaScript 错误；",[126,3508,3509],{},"本地 Nuxt Dev 是否还在首次编译；",[126,3511,3512],{},"本地内容 SQLite 缓存是否损坏；",[126,3514,3515],{},"大型依赖或资源是否阻塞客户端入口；",[126,3517,3518],{},"当前访问的是临时 Nuxt Dev，还是已经生成的 3000\u002F3100 静态站。",[119,3520,3521],{},"目前通过移除字体、减轻入口资源、预加载 mask、分离内容数据库和明确动画水合闸门改善了这个过程。远程测试站也已经改成静态 release，不再边访问边编译，因此它与正式站具有相同的水合特征。",[110,3523,3525],{"id":3524},"三十常见修改应该改哪里","三十、常见修改应该改哪里",[115,3527,3529],{"id":3528},"修改个人介绍栏目文案或服务","修改个人介绍、栏目文案或服务",[119,3531,3532,3533,3535,3536,3538],{},"优先通过 CMS 中对应的页面入口，或编辑 ",[62,3534,1196],{}," 下对应 YAML；服务列表位于 ",[62,3537,1395],{},"。若新增服务状态或图标类型，还要同步修改 Schema、配置枚举、CMS 选项、组件映射、CSS 和测试。",[115,3540,3541],{"id":3541},"修改默认颜色",[119,3543,3544,3545,3547,3548,3550],{},"修改 ",[62,3546,1778],{},"。它可以是任意 ",[62,3549,1442],{},"。已有访客若保存过自定义颜色，会继续看到自己的选择；要做全体迁移，需要像旧黄色迁移一样明确识别并处理旧值，不能无条件清空 localStorage。",[115,3552,3553],{"id":3553},"修改红绿蓝预设",[119,3555,3544,3556,3558,3559,3562],{},[62,3557,1714],{}," 的 ",[62,3560,3561],{},"accentPresets","。RGB 面板和非主页调色按钮都复用它。修改后应测试纯黑纯白对比、localStorage 恢复和字标重播。",[115,3564,3565],{"id":3565},"修改艺术字",[119,3567,3568,3569,3558,3571,3573,3574,3576,3577,238],{},"在 CMS 中打开“站点设置 \u002F 首页”，修改“艺术字文字”即可。它最终写入 ",[62,3570,1375],{},[62,3572,1988],{},"，保存到测试分支后由 Webhook 自动重新构建测试站。字形由 ",[62,3575,1214],{}," 引入，字体栈、字号和四层颜色位于 ",[62,3578,3579],{},"main.css",[115,3581,3582],{"id":3582},"修改字标动画",[119,3584,3585,3586,3588,3589,3592,3593,3595],{},"播放条件在 ",[62,3587,2035],{},"，跨组件触发在 ",[62,3590,3591],{},"useWordmarkAnimation.ts","，视觉关键帧在 ",[62,3594,3579],{},"。不要把三者混成一个大组件：字体状态、事件状态和视觉时间线是不同责任。",[115,3597,3598],{"id":3598},"修改文章分类",[119,3600,3601,3602,3605],{},"至少同步 ",[62,3603,3604],{},"app\u002Fconfig\u002Farticles.ts","、Nuxt Content Schema、CMS 选项、校验器测试与界面测试。文章中的旧分类是否迁移，需要单独决定。",[115,3607,3608],{"id":3608},"修改文章样式",[119,3610,3611,3612,3615,3616,3618,3619,3622],{},"文章列表看 ",[62,3613,3614],{},".article-row","，正文看 ",[62,3617,2316],{},"，详情头部看 ",[62,3620,3621],{},".article-hero","。长标题、手机宽度、hover 前后几何和其他文章 opacity 是必须复查的回归点。",[115,3624,3625],{"id":3625},"修改生产或测试服务",[119,3627,3628,3629,3631],{},"静态响应逻辑改 ",[62,3630,2953],{},"；监听端口、release 根目录、Node 路径和重启策略改对应 systemd unit。同步分支、代理、独立仓库和部署锁改两个 sync unit。只有静态服务器或 unit 变化需要重启\u002F重载，普通 release 切换不需要。",[110,3633,3635],{"id":3634},"三十一日常发布配方","三十一、日常发布配方",[115,3637,3639],{"id":3638},"通过远程-cms-修改内容","通过远程 CMS 修改内容",[277,3641,3642,3648,3651,3656],{},[126,3643,3644,3645,3647],{},"打开 ",[62,3646,2573],{}," 并保存。",[126,3649,3650],{},"等待 Webhook 触发 staging 构建，检查测试站主页、列表和具体 URL。",[126,3652,3653,3654,238],{},"在干净且最新的 master 工作区运行 ",[62,3655,2559],{},[126,3657,3658],{},"等待 master Webhook 触发生产构建，再检查正式站。",[119,3660,3661],{},"保存、预览、晋级和正式部署是四个明确阶段。隐藏文章也遵循同一流程。",[115,3663,3664],{"id":3664},"本地受信流程直接发布内容",[357,3666,3670],{"className":3667,"code":3668,"language":3669,"meta":362,"style":362},"language-bash shiki shiki-themes github-light github-dark","npm run content:validate\nnpm run publish\n","bash",[62,3671,3672,3683],{"__ignoreMap":362},[366,3673,3674,3677,3680],{"class":368,"line":369},[366,3675,3676],{"class":1562},"npm",[366,3678,3679],{"class":1569}," run",[366,3681,3682],{"class":1569}," content:validate\n",[366,3684,3685,3687,3689],{"class":368,"line":537},[366,3686,3676],{"class":1562},[366,3688,3679],{"class":1569},[366,3690,3691],{"class":1569}," publish\n",[119,3693,3694,3696],{},[62,3695,2847],{}," 成功后会输出 commit、release 和 current 路径。它用于 OpenClaw 等本地受限流程，会直接切换默认生产 release；是否推送 GitHub 是另一项操作。普通远程 CMS 编辑不要使用这条路径绕过 staging。",[115,3698,3700],{"id":3699},"发布-vuecss脚本或配置代码","发布 Vue、CSS、脚本或配置代码",[357,3702,3704],{"className":3667,"code":3703,"language":3669,"meta":362,"style":362},"npm run typecheck\nnpm run test:content\nnpm run test:release\nnpm run test:e2e\nnpm run generate\ngit diff --check\n# 明确检查并提交本次文件\ngit push origin master\n",[62,3705,3706,3715,3724,3733,3742,3751,3762,3768],{"__ignoreMap":362},[366,3707,3708,3710,3712],{"class":368,"line":369},[366,3709,3676],{"class":1562},[366,3711,3679],{"class":1569},[366,3713,3714],{"class":1569}," typecheck\n",[366,3716,3717,3719,3721],{"class":368,"line":537},[366,3718,3676],{"class":1562},[366,3720,3679],{"class":1569},[366,3722,3723],{"class":1569}," test:content\n",[366,3725,3726,3728,3730],{"class":368,"line":35},[366,3727,3676],{"class":1562},[366,3729,3679],{"class":1569},[366,3731,3732],{"class":1569}," test:release\n",[366,3734,3735,3737,3739],{"class":368,"line":1759},[366,3736,3676],{"class":1562},[366,3738,3679],{"class":1569},[366,3740,3741],{"class":1569}," test:e2e\n",[366,3743,3744,3746,3748],{"class":368,"line":1769},[366,3745,3676],{"class":1562},[366,3747,3679],{"class":1569},[366,3749,3750],{"class":1569}," generate\n",[366,3752,3753,3756,3759],{"class":368,"line":2379},[366,3754,3755],{"class":1562},"git",[366,3757,3758],{"class":1569}," diff",[366,3760,3761],{"class":1746}," --check\n",[366,3763,3764],{"class":368,"line":2390},[366,3765,3767],{"class":3766},"sJ8bj","# 明确检查并提交本次文件\n",[366,3769,3770,3772,3775,3778],{"class":368,"line":2399},[366,3771,3755],{"class":1562},[366,3773,3774],{"class":1569}," push",[366,3776,3777],{"class":1569}," origin",[366,3779,3780],{"class":1569}," master\n",[119,3782,3783,3784,3786,3787,3789],{},"master Webhook 会触发同步器再次执行完整 ",[62,3785,2710],{},"，前面的命令适合在提交前尽早发现问题。需要在 VPS 立即部署当前干净提交时也可以手工运行 ",[62,3788,2710],{},"。代码版本只进入 master 时，测试站可能暂时仍运行上一份代码；下一次内容晋级会把最新 master 与 staging 合并并重新对齐两边。",[115,3791,3792],{"id":3792},"查看当前生产状态",[357,3794,3796],{"className":3667,"code":3795,"language":3669,"meta":362,"style":362},"systemctl --user is-active personal-site.service\nsystemctl --user is-active personal-site-dev.service\nreadlink -f ~\u002F.local\u002Fshare\u002Fpersonal-site\u002Fcurrent\nreadlink -f ~\u002F.local\u002Fshare\u002Fpersonal-site-staging\u002Fcurrent\ncurl -fsS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type}\\n' http:\u002F\u002F127.0.0.1:3000\u002F\ncurl -fsS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type}\\n' http:\u002F\u002F127.0.0.1:3100\u002F\n",[62,3797,3798,3812,3823,3834,3843,3866],{"__ignoreMap":362},[366,3799,3800,3803,3806,3809],{"class":368,"line":369},[366,3801,3802],{"class":1562},"systemctl",[366,3804,3805],{"class":1746}," --user",[366,3807,3808],{"class":1569}," is-active",[366,3810,3811],{"class":1569}," personal-site.service\n",[366,3813,3814,3816,3818,3820],{"class":368,"line":537},[366,3815,3802],{"class":1562},[366,3817,3805],{"class":1746},[366,3819,3808],{"class":1569},[366,3821,3822],{"class":1569}," personal-site-dev.service\n",[366,3824,3825,3828,3831],{"class":368,"line":35},[366,3826,3827],{"class":1562},"readlink",[366,3829,3830],{"class":1746}," -f",[366,3832,3833],{"class":1569}," ~\u002F.local\u002Fshare\u002Fpersonal-site\u002Fcurrent\n",[366,3835,3836,3838,3840],{"class":368,"line":1759},[366,3837,3827],{"class":1562},[366,3839,3830],{"class":1746},[366,3841,3842],{"class":1569}," ~\u002F.local\u002Fshare\u002Fpersonal-site-staging\u002Fcurrent\n",[366,3844,3845,3848,3851,3854,3857,3860,3863],{"class":368,"line":1769},[366,3846,3847],{"class":1562},"curl",[366,3849,3850],{"class":1746}," -fsS",[366,3852,3853],{"class":1746}," -o",[366,3855,3856],{"class":1569}," \u002Fdev\u002Fnull",[366,3858,3859],{"class":1746}," -w",[366,3861,3862],{"class":1569}," '%{http_code} %{content_type}\\n'",[366,3864,3865],{"class":1569}," http:\u002F\u002F127.0.0.1:3000\u002F\n",[366,3867,3868,3870,3872,3874,3876,3878,3880],{"class":368,"line":2379},[366,3869,3847],{"class":1562},[366,3871,3850],{"class":1746},[366,3873,3853],{"class":1746},[366,3875,3856],{"class":1569},[366,3877,3859],{"class":1746},[366,3879,3862],{"class":1569},[366,3881,3882],{"class":1569}," http:\u002F\u002F127.0.0.1:3100\u002F\n",[115,3884,3885],{"id":3885},"回滚",[119,3887,3888],{},"先列出确实存在的 release，再执行：",[357,3890,3892],{"className":3667,"code":3891,"language":3669,"meta":362,"style":362},"npm run rollback -- \u003Cexact-release-id>\n",[62,3893,3894],{"__ignoreMap":362},[366,3895,3896,3898,3900,3903,3906,3909,3912,3915],{"class":368,"line":369},[366,3897,3676],{"class":1562},[366,3899,3679],{"class":1569},[366,3901,3902],{"class":1569}," rollback",[366,3904,3905],{"class":1746}," --",[366,3907,3908],{"class":1727}," \u003C",[366,3910,3911],{"class":1569},"exact-release-i",[366,3913,3914],{"class":1554},"d",[366,3916,1581],{"class":1727},[119,3918,3919,3920,3922,3923,3925,3926,238],{},"回滚后再次检查 ",[62,3921,974],{}," 和 3000 返回值。不要手动删除 ",[62,3924,974],{},"，也不要把它指向仓库的 ",[62,3927,970],{},[110,3929,3931],{"id":3930},"三十二故障排查顺序","三十二、故障排查顺序",[115,3933,3935],{"id":3934},"cms-保存后测试站看不到","CMS 保存后测试站看不到",[119,3937,3938,3939,3941,3942,3944,3945,1186,3948,3951,3952,3955,3956,3958],{},"先确认提交是否进入 GitHub ",[62,3940,915],{},"，并在 GitHub Recent Deliveries 中确认 Webhook 返回 ",[62,3943,2807],{},"；再检查 ",[62,3946,3947],{},"personal-site-webhook.service",[62,3949,3950],{},"personal-site-staging-sync.service"," 日志、",[62,3953,3954],{},"~\u002F.local\u002Fstate\u002Fpersonal-site-staging-sync\u002Fdeployed-commit"," 和测试 ",[62,3957,974],{},"。构建失败不会覆盖旧预览；事件文件会保留并延迟重试，也可以修复后手工启动同步服务。",[115,3960,3961],{"id":3961},"测试站正确但正式站没变化",[119,3963,3964,3965,3967,3968,3970,3971,3974,3975,3977],{},"确认是否实际运行过 ",[62,3966,2559],{},"、远端 master 是否已更新以及 master Webhook 是否返回 ",[62,3969,2807],{},"，再检查 ",[62,3972,3973],{},"personal-site-sync.service"," 和生产 ",[62,3976,2766],{},"。CMS 保存只更新测试站是正常设计，不是同步故障。",[115,3979,3981],{"id":3980},"rgb-不能拖按钮不能点动画不播放","RGB 不能拖、按钮不能点、动画不播放",[119,3983,3984,3985,3988,3989,3992],{},"先看 ",[62,3986,3987],{},"data-hydrated","，再看控制台错误和网络请求。若只发生在临时 Nuxt Dev 的第一次编译，等待完成后重载；若持续发生，停止那个临时开发进程，运行 ",[62,3990,3991],{},"npx nuxt cleanup"," 后重启开发端口。常驻 3000 和 3100 都是静态服务器，不使用这份开发缓存。",[115,3994,3995],{"id":3995},"切回标签页没有动画",[119,3997,3998,3999,1186,4001,4003],{},"检查浏览器是否启用 reduced motion、mask 图片是否加载失败，以及 ",[62,4000,2121],{},[62,4002,2124],{}," 是否触发。一次切回只播一次是正常防抖，不应去掉 200ms 合并后让三个事件连续触发。",[115,4005,4006],{"id":4006},"换色先闪回旧颜色",[119,4008,4009,4010,4012],{},"检查静态默认色、Nuxt ",[62,4011,1782],{}," 初始值和 localStorage 恢复顺序。服务端 HTML无法知道客户端保存颜色，完全避免首帧差异需要在页面渲染前执行极小的内联主题恢复脚本；当前实现选择保持静态生成简单，在水合时恢复。",[115,4014,4016],{"id":4015},"服务状态点不随-cms-变化","服务状态点不随 CMS 变化",[119,4018,4019,4020,4023,4024,4026],{},"确认 YAML 中 ",[62,4021,4022],{},"state"," 已保存、值属于三个枚举，并检查正在看的环境：CMS 保存后测试站应先变化，正式站要等 promote。若 DOM 属性已变而颜色没变，检查 CSS 状态选择器；若属性没变，检查对应环境的 ",[62,4025,974],{}," 是否仍是旧 release。",[115,4028,4030],{"id":4029},"_3000-无法访问","3000 无法访问",[119,4032,4033,4034,4036,4037,4039],{},"检查用户级 systemd 状态、日志、Node 固定路径、端口占用和 ",[62,4035,974],{}," 链接。服务器进程存在但 ",[62,4038,974],{}," 失效时会返回 503。不要在未确认进程归属前直接杀端口进程。",[119,4041,4042,4043,4046,4047,4049],{},"3100 使用相同排查方法，但服务是 ",[62,4044,4045],{},"personal-site-dev.service","，数据根目录是 ",[62,4048,2929],{},"。正式站正常而测试站异常时，不要重启 3000。",[115,4051,4053],{"id":4052},"管理页面提示只允许-https","管理页面提示只允许 HTTPS",[119,4055,4056,4057,4059],{},"这通常来自浏览器安全上下文要求，而不是 Git remote 配错。Git remote 只决定仓库同步地址，不会给 HTTP 页面增加 HTTPS。远程后台应使用已经配置可信证书和 Basic Auth 的 ",[62,4058,2573],{},"；本地仓库模式则从 localhost 打开。",[110,4061,4063],{"id":4062},"三十三安全与备份边界","三十三、安全与备份边界",[119,4065,4066],{},"至少要区分三类数据：",[357,4068,4071],{"className":4069,"code":4070,"language":943,"meta":362},[941],"Git 仓库       源码、文章、站点设置、公开静态资源\nOpenClaw 目录  Skill、草稿、会话和本地配置\nrelease 目录   生产与测试当前及历史静态构建结果\n",[62,4072,4070],{"__ignoreMap":362},[119,4074,4075],{},"Git 提交是版本历史，不是异地备份。私人远程仓库可以备份源码与文章，但 OpenClaw 配置和 release 数据需要单独备份到受控存储。",[119,4077,4078,4079,4081],{},"永远不要提交或写入公开文章的内容包括：API key、OAuth secret、部署私钥、QQ token、cookie、扫码状态、",[62,4080,2566],{},"、私人服务密码、内部地址清单和不适合公开的资产信息。",[119,4083,4084,4085,4088,4089,4091],{},"CMS 的 ",[62,4086,4087],{},"\u002Fadmin\u002F"," 路径和 ",[62,4090,2577],{}," 只能减少搜索引擎收录，不能阻止陌生人访问。当前远程后台同时依靠可信 HTTPS、Nginx Basic Auth 和 GitHub OAuth；生产\u002F测试上游只监听回环地址，公网 IP 与未知 Host 由默认 Nginx 站点拒绝。真正的保护来自这些边界和最小权限凭据，而不是隐藏 URL。",[110,4093,4095],{"id":4094},"三十四维护这套代码时应坚持的原则","三十四、维护这套代码时应坚持的原则",[119,4097,4098],{},"第一，内容、表现和发布边界分开。能在 YAML 解决的文案不要硬编码进 Vue；能由共享组件表达的状态不要在两个页面复制；生产切换不要混进页面代码。",[119,4100,4101],{},"第二，把浏览器状态当成显式状态。主题色、资源是否解码、页面是否可见、指针是否仍按下、水合是否完成，都应有清楚的变量和生命周期，而不是靠延时猜测。",[119,4103,4104],{},"第三，动画必须有静态最终状态。资源失败、JavaScript 失败或用户减少动态效果时，标题仍要可读，布局仍要成立。",[119,4106,4107],{},"第四，自动化写入必须比人工编辑权限更窄。Skill 负责工作流，writer 负责硬边界，validator 负责内容合约，release 负责上线，四者不能互相冒充。",[119,4109,4110],{},"第五，生产部署应该是“生成一个完整新版本，然后切换”，而不是在访问者正在读取的目录里逐个覆盖文件。原子 release 是这个小网站最值得长期保留的工程设计之一。",[119,4112,4113,4114,4116],{},"第六，测试站和正式站必须由分支、仓库、release 与进程共同隔离。域名不同不等于环境隔离，CMS 保存也不等于生产发布；",[62,4115,919],{}," 是有校验的明确边界。",[119,4118,4119],{},"第七，测试真实的关系和历史 bug。主题对比、滑杆松手、标签页重播、长标题 hover、隐藏文章 404、服务状态同步和多视口不重叠，都来自实际使用中的问题，比为了提高数字而堆大量脆弱测试更有价值。",[110,4121,4123],{"id":4122},"三十五重新上手时的最短阅读路径","三十五、重新上手时的最短阅读路径",[119,4125,4126],{},"如果未来很久没有维护这个项目，按下面顺序读，可以最快恢复上下文：",[277,4128,4129,4135,4143,4152,4158,4163,4175,4178],{},[126,4130,4131,4132,4134],{},"看 ",[62,4133,1196],{}," 的五份 YAML，知道全站和各页面当前展示什么。",[126,4136,4131,4137,1201,4140,4142],{},[62,4138,4139],{},"app\u002Fpages\u002Findex.vue",[62,4141,3579],{}," 的主页部分，理解首屏结构。",[126,4144,4131,4145,1186,4147,1186,4149,4151],{},[62,4146,1714],{},[62,4148,1897],{},[62,4150,2035],{},"，理解最复杂的交互。",[126,4153,4154,4155,4157],{},"看两个 writing 页面和 ",[62,4156,1283],{},"，理解文章查询与渲染。",[126,4159,4131,4160,4162],{},[62,4161,1433],{},"、validator 和 writer，理解内容边界。",[126,4164,4131,4165,1186,4168,1186,4171,4174],{},[62,4166,4167],{},"release.ts",[62,4169,4170],{},"sync.ts",[62,4172,4173],{},"promote.ts"," 和 systemd unit，理解双环境发布链路。",[126,4176,4177],{},"看 Nginx 模板与运维文档，理解域名、HTTPS、Basic Auth 和回环上游。",[126,4179,4180],{},"最后看测试，把“哪些行为不能再坏”快速过一遍。",[119,4182,4183],{},"只要能重新回答下面五个问题，就已经掌握了这个项目：",[123,4185,4186,4189,4192,4195,4198,4201],{},[126,4187,4188],{},"当前页面内容的唯一来源在哪里？",[126,4190,4191],{},"浏览器中的主题色如何从 RGB 状态传到 CSS、字标和 favicon？",[126,4193,4194],{},"为什么 CMS 保存后只改变测试站？",[126,4196,4197],{},"publish、deploy、sync 和 promote 的权限、检查与 Git 行为有什么不同？",[126,4199,4200],{},"一个新 release 如何在不重启 3000 的情况下接管生产请求？",[126,4202,4203],{},"3000 与 3100 为什么不会覆盖彼此的仓库和 current？",[119,4205,4206],{},"这些问题就是 Quilibra 当前技术架构的骨架。",[110,4208,4209],{"id":4209},"来源与延伸阅读",[123,4211,4212,4221,4228,4235,4242,4249,4256,4263],{},[126,4213,4214],{},[4215,4216,4220],"a",{"href":4217,"rel":4218},"https:\u002F\u002Fnuxt.com\u002Fdocs",[4219],"nofollow","Nuxt 文档",[126,4222,4223],{},[4215,4224,4227],{"href":4225,"rel":4226},"https:\u002F\u002Fcontent.nuxt.com\u002F",[4219],"Nuxt Content 文档",[126,4229,4230],{},[4215,4231,4234],{"href":4232,"rel":4233},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fextras\u002Fcomposition-api-faq.html",[4219],"Vue Composition API",[126,4236,4237],{},[4215,4238,4241],{"href":4239,"rel":4240},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Fintro",[4219],"Playwright 文档",[126,4243,4244],{},[4215,4245,4248],{"href":4246,"rel":4247},"https:\u002F\u002Fgithub.com\u002Fsveltia\u002Fsveltia-cms",[4219],"Sveltia CMS 文档",[126,4250,4251],{},[4215,4252,4255],{"href":4253,"rel":4254},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002Fmask",[4219],"MDN：CSS mask",[126,4257,4258],{},[4215,4259,4262],{"href":4260,"rel":4261},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FPage_Visibility_API",[4219],"MDN：Page Visibility API",[126,4264,4265],{},[4215,4266,4269],{"href":4267,"rel":4268},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FCaching",[4219],"MDN：HTTP 缓存",[533,4271,4272],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":362,"searchDepth":537,"depth":537,"links":4274},[4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4294,4295,4296,4303,4304,4305,4306,4307,4312,4313,4314,4324,4331,4341,4342,4343,4344],{"id":930,"depth":537,"text":931},{"id":981,"depth":537,"text":982},{"id":1014,"depth":537,"text":1015},{"id":1144,"depth":537,"text":1145},{"id":1208,"depth":537,"text":1209},{"id":1340,"depth":537,"text":1341},{"id":1485,"depth":537,"text":1486},{"id":1598,"depth":537,"text":1599},{"id":1669,"depth":537,"text":1670},{"id":1707,"depth":537,"text":1708},{"id":1830,"depth":537,"text":1831},{"id":1891,"depth":537,"text":1892},{"id":1977,"depth":537,"text":1978},{"id":2074,"depth":537,"text":2075},{"id":2148,"depth":537,"text":2149},{"id":2219,"depth":537,"text":2220},{"id":2278,"depth":537,"text":2279},{"id":2320,"depth":537,"text":2321},{"id":2495,"depth":537,"text":2496},{"id":2584,"depth":537,"text":2585},{"id":2643,"depth":537,"text":2644},{"id":2682,"depth":537,"text":2683,"children":4297},[4298,4299,4300,4301,4302],{"id":2689,"depth":35,"text":2692},{"id":2707,"depth":35,"text":2710},{"id":2722,"depth":35,"text":2725},{"id":2779,"depth":35,"text":2782},{"id":2813,"depth":35,"text":2559},{"id":2864,"depth":537,"text":2865},{"id":2947,"depth":537,"text":2948},{"id":3089,"depth":537,"text":3090},{"id":3161,"depth":537,"text":3162},{"id":3261,"depth":537,"text":3262,"children":4308},[4309,4310,4311],{"id":3268,"depth":35,"text":3269},{"id":3309,"depth":35,"text":3310},{"id":3353,"depth":35,"text":3354},{"id":3407,"depth":537,"text":3408},{"id":3490,"depth":537,"text":3491},{"id":3524,"depth":537,"text":3525,"children":4315},[4316,4317,4318,4319,4320,4321,4322,4323],{"id":3528,"depth":35,"text":3529},{"id":3541,"depth":35,"text":3541},{"id":3553,"depth":35,"text":3553},{"id":3565,"depth":35,"text":3565},{"id":3582,"depth":35,"text":3582},{"id":3598,"depth":35,"text":3598},{"id":3608,"depth":35,"text":3608},{"id":3625,"depth":35,"text":3625},{"id":3634,"depth":537,"text":3635,"children":4325},[4326,4327,4328,4329,4330],{"id":3638,"depth":35,"text":3639},{"id":3664,"depth":35,"text":3664},{"id":3699,"depth":35,"text":3700},{"id":3792,"depth":35,"text":3792},{"id":3885,"depth":35,"text":3885},{"id":3930,"depth":537,"text":3931,"children":4332},[4333,4334,4335,4336,4337,4338,4339,4340],{"id":3934,"depth":35,"text":3935},{"id":3961,"depth":35,"text":3961},{"id":3980,"depth":35,"text":3981},{"id":3995,"depth":35,"text":3995},{"id":4006,"depth":35,"text":4006},{"id":4015,"depth":35,"text":4016},{"id":4029,"depth":35,"text":4030},{"id":4052,"depth":35,"text":4053},{"id":4062,"depth":537,"text":4063},{"id":4094,"depth":537,"text":4095},{"id":4122,"depth":537,"text":4123},{"id":4209,"depth":537,"text":4209},"2026-07-26","记录 Quilibra 个人主页 2026 年 7 月版本的技术选型、页面架构、内容系统、主题动画、双环境发布、HTTPS、测试体系和维护方法。",{},"\u002Fwriting\u002F2026-07-26-quilibra-complete-technical-handbook","2026-07-26T23:46:23+08:00",{"title":898,"description":4346},"quilibra-complete-technical-handbook","writing\u002F2026-07-26-quilibra-complete-technical-handbook",[4354,4355,1053,4356,1101,1093,4357],"Nuxt","Vue","Nuxt Content","静态部署","2026-08-03","ILFf8HTh9BFuZakheSV0lJmjpNenfU1fBRtcsKT1umE",{"id":4361,"title":4362,"body":4363,"category":4513,"date":4345,"description":4514,"extension":561,"hidden":562,"meta":4515,"navigation":31,"path":4516,"publishedAt":4349,"seo":4517,"slug":4518,"stem":4519,"tags":4520,"updated":4345,"__hash__":4525},"writing\u002Fwriting\u002F2026-07-26-trading-cognition-and-compound-reality.md","交易是为了赚钱还是验证认知：从量化门槛到复利现实",{"type":107,"value":4364,"toc":4502},[4365,4368,4372,4375,4378,4382,4385,4388,4391,4394,4397,4400,4403,4407,4410,4430,4433,4444,4447,4450,4464,4467,4470,4473,4476,4479,4483,4486,4489,4493,4496,4499],[119,4366,4367],{},"刷到一个清华博士的视频，说想赚钱就去量化。但这对于普通人的门槛是不是太高？就算跨过去了，市场容量真的够赚到钱吗？带着这些问题和 AI 聊了一圈，从量化门槛一路聊到\"你到底为什么做交易\"，把思路理了一遍。",[110,4369,4371],{"id":4370},"机构量化-vs-系统化交易","机构量化 vs 系统化交易",[119,4373,4374],{},"机构量化（幻方、九坤级别）的门槛确实很高：博士学历、海量 tick 数据、专用算力、上亿起步资金，而且赛道内卷、alpha 衰减快、容量有限。",[119,4376,4377],{},"但普通人能做的是\"系统化交易\"而非机构量化。20 日均线 + 严格止损 + 行业 ETF，这套逻辑本质就是量化思维。真正的问题不是门槛，是把博士生的参照系错误地套用在自己身上。这类建议对清北学生可操作，对大多数人是废话，因为它省略了关键前提：清北学生的起点已经过滤掉 90% 的竞争。",[110,4379,4381],{"id":4380},"只是为了赚钱可能是对自己的贬低","\"只是为了赚钱\"可能是对自己的贬低",[119,4383,4384],{},"我说做交易只是为了赚钱。但仔细想想，我对系统和机制本身一直有探究欲--止损逻辑、去中心化的交易所架构、自己项目的技术选型会追求优雅和性能。真正纯粹为钱的人不会纠结止损后策略逻辑对不对。",[119,4386,4387],{},"如果账户明天清零，还会不会继续研究这套系统？答案可能是会的。因为做交易挣不挣钱是对我认知是否正确的检验，猜对了奖励，猜错了亏钱。我在市场里的目的是验证认知，想知道自己是怎样的人。",[119,4389,4390],{},"核心驱动不是钱（AI说的 我持怀疑态度），是\"认知闭环\"--市场是不会说谎的裁判，钱只是计分板。",[110,4392,4393],{"id":4393},"一个需要警惕的特质",[119,4395,4396],{},"对\"是否正确\"极度敏感的人，容易把坚持系统等同于坚持自我，把止损等同于承认\"我是错的\"。如果哪天开始扛单不是因为信号而是无法接受\"我看错了\"，这个特质就从优势变成风险。",[119,4398,4399],{},"关键区分：把\"验证认知\"和\"验证自我价值\"分开。市场说这笔交易错了，不等于整个判断体系错了。我相信市场大部分时间是对的，应该是我往市场靠去检验自己，而不是市场错的时候坚持自己。但空仓时说这话容易，真正重仓浮亏 5%、系统说止损但脑子里说\"只是短期波动\"的那一刻，这套认知还在不在才是真考验。",[119,4401,4402],{},"止损系统的价值不只是风控，更是强制执行那些理性认同但情绪会动摇的信念。",[110,4404,4406],{"id":4405},"_10-万到几百万数学可行执行淘汰大多数人","10 万到几百万：数学可行，执行淘汰大多数人",[119,4408,4409],{},"市场容量直接排除--A 股日均成交额 8000 亿到 1.2 万亿，几十万到几百万对市场影响约等于零。真实难度分三段：",[123,4411,4412,4418,4424],{},[126,4413,4414,4417],{},[129,4415,4416],{},"10 万到 100 万","：拼策略稳定性和纪律一致性",[126,4419,4420,4423],{},[129,4421,4422],{},"100 万到 500 万","：拼复利需要时间，以及账户变大后心态是否变化（100 万亏 10% 是 10 万，感受和 10 万亏 10% 完全不同）",[126,4425,4426,4429],{},[129,4427,4428],{},"500 万以上","：拼市场风格切换时系统能否不变形",[119,4431,4432],{},"参照现有系统严格执行，年化 15%-30% 的历史区间成立。三种情景的复利推算：",[123,4434,4435,4438,4441],{},[126,4436,4437],{},"**保守 15%**：10 万到 100 万约 17 年，到 500 万约 39 年",[126,4439,4440],{},"**中性 20%**：10 万到 100 万约 13 年，到 500 万约 21 年",[126,4442,4443],{},"**乐观 25%**：10 万到 100 万约 11 年，到 500 万约 15 年",[119,4445,4446],{},"数学上可行，但\"10 年严格执行\"淘汰的人比策略本身多。年化均值背后有巨大的方差，最大风险不是策略本身，而是震荡市连续止损后大多数人会改变规则。",[110,4448,4449],{"id":4449},"人群中能走到哪",[123,4451,4452,4455,4458,4461],{},[126,4453,4454],{},"能长期跑赢通胀：前 20%",[126,4456,4457],{},"年化 15%+ 持续 5 年以上：前 5%",[126,4459,4460],{},"系统化策略 10 年不变形：前 2-3%",[126,4462,4463],{},"10 万做到 500 万：前 1-2%",[119,4465,4466],{},"这个门槛不是天才门槛，而是纪律和时间门槛。输给认知的少，输给自己的多。",[110,4468,4469],{"id":4469},"交易不是生活的全部",[119,4471,4472],{},"正常认真上班拿现金流，有自己的爱好爱好，偶尔玩玩游戏，继续交易探索市场和世界。每个部分各司其职：工作管现金流，运动游戏管切换放松，交易是认知战场。",[119,4474,4475],{},"这个心态本身是优势。很多人把交易当生活全部，盯盘过度、情绪放大、决策变差。有工作提供现金流、有运动游戏切换状态，交易在这个结构里心理压力更小，压力小反而更容易执行系统。",[119,4477,4478],{},"但有一个前提：执行密度。想清楚和做到之间有 gap，关键不是今天想通了，而是明天还在做。",[110,4480,4482],{"id":4481},"一个佩服的-up-主","一个佩服的 UP 主",[119,4484,4485],{},"关注过一个 UP 主，清华毕业三十多岁，提过资产千万级。他的操作和认知确实让人佩服：航运信息差套利没记错一天挣了20w，6月8日暴跌开中证 500 期权多单为国护盘，科技股暴跌后判断主线仍是科技股事后看来也是对的。他还分享打新、股东福利等内容。",[119,4487,4488],{},"他的宏观判断和产业理解思路值得学，具体操作和套利思路也有参考价值。没有实盘记录这点确实无法验证，但判断以及操作确实很牛逼。",[110,4490,4492],{"id":4491},"a-股打新现状","A 股打新现状",[119,4494,4495],{},"查了一下 A 股打新的数据：2024 年新股首日平均涨幅 253%，单签平均收益约 2 万元；2025 年至 6 月，45 只新股无一破发。但主板网上中签率仅 0.03% 左右，参与人数超 1100 万户，普通人中签概率极低，更像是彩票。网下机构打新年化约 4.5%，但门槛是亿级资金账户。包括今年以来的一些大肉签（比如沐曦股份 摩尔线程 最近的长鑫存储）上市当日涨幅都很夸张，随便中几签都是几万块。可惜科创板门槛高啊。（我什么时候才能有50w）",[110,4497,4498],{"id":4498},"止损永远是最重要的",[119,4500,4501],{},"按照均线止损太死板，套到很多个股上买点太高了。但不管用什么方式入场，止损永远是第一位的。买点可以慢慢校准，进场理由可以有很多种，但止损必须进场前定好，而且不能因为浮亏就移动它。止损一旦守不住，什么策略都白搭。在市场里还是先活下来比较重要。",{"title":362,"searchDepth":537,"depth":537,"links":4503},[4504,4505,4506,4507,4508,4509,4510,4511,4512],{"id":4370,"depth":537,"text":4371},{"id":4380,"depth":537,"text":4381},{"id":4393,"depth":537,"text":4393},{"id":4405,"depth":537,"text":4406},{"id":4449,"depth":537,"text":4449},{"id":4469,"depth":537,"text":4469},{"id":4481,"depth":537,"text":4482},{"id":4491,"depth":537,"text":4492},{"id":4498,"depth":537,"text":4498},"交易","从清华博士的建议想要赚钱就去做量化出发，梳理普通人做系统化交易的现实路径、人群比例和自我认知驱动。",{},"\u002Fwriting\u002F2026-07-26-trading-cognition-and-compound-reality",{"title":4362,"description":4514},"trading-cognition-and-compound-reality","writing\u002F2026-07-26-trading-cognition-and-compound-reality",[4513,4521,4522,4523,4524],"量化","认知","复利","系统化交易","ncRskTdQt3kCImby9qKSkM5aUBbcdCb-MjUngbbkfEs",{"id":4527,"title":4528,"body":4529,"category":4513,"date":4345,"description":4705,"extension":561,"hidden":562,"meta":4706,"navigation":31,"path":4707,"publishedAt":4708,"seo":4709,"slug":4710,"stem":4711,"tags":4712,"updated":578,"__hash__":4719},"writing\u002Fwriting\u002F2026-07-26-btc-synthetic-futures-arbitrage.md","BTC期权合成期货套利：用定价偏差锁定无风险利润",{"type":107,"value":4530,"toc":4696},[4531,4534,4538,4541,4544,4548,4554,4562,4565,4568,4571,4577,4583,4586,4590,4593,4596,4600,4607,4613,4616,4619,4633,4638,4646,4652,4657,4668,4673,4683,4686,4690,4693],[119,4532,4533],{},"看到一套量化交易系统看板：年化 197%、极低回撤、多平仓少持仓。核心策略是利用 Deribit 期权市场合成的虚拟期货价格与 Binance 永续合约价格之间的定价偏差进行对冲套利。当时对着看板追问了 AI 几轮，才把这套合成期货套利的逻辑理清楚。这篇文章是给自己留的复盘笔记。",[110,4535,4537],{"id":4536},"deribit-是什么","Deribit 是什么",[119,4539,4540],{},"Deribit 是目前加密货币期权交易量最大的交易所，总部在巴拿马，2016 年上线。BTC 和 ETH 期权的主流流动性都在这里，行权价、到期日覆盖范围广，机构参与度高。它的期权以币本位结算（BTC 期权用 BTC 结算），也支持永续合约和交割期货。",[119,4542,4543],{},"正因为 Deribit 在期权市场的深度优势，它的期权定价成为合成期货策略的基础端。另一端选 Binance 永续合约，是因为 Binance 流动性最好、API 稳定、支持开空，适合做对冲腿。",[110,4545,4547],{"id":4546},"核心概念合成期货","核心概念：合成期货",[119,4549,4550,4551,238],{},"期权平价公式告诉我们：",[129,4552,4553],{},"买入 Call + 卖出 Put = 合成期货多头",[123,4555,4556,4559],{},[126,4557,4558],{},"买入 Call（看涨期权）：涨了赚，跌了不亏",[126,4560,4561],{},"卖出 Put（看跌期权）：涨了没事，跌了亏",[119,4563,4564],{},"两者组合后的盈亏轨迹，与直接持有 BTC 现货或期货完全一致——涨了赚、跌了亏。这就是\"合成期货\"。",[110,4566,4567],{"id":4567},"两种套利方向",[119,4569,4570],{},"关键在于：Deribit 期权合成的期货价格，和 Binance 永续合约的价格，经常不一致。",[119,4572,4573,4576],{},[129,4574,4575],{},"转换套利（Conversion \u002F SF+BS）","：当合成期货价格 > Binance 期货价格时，在 Deribit 卖出合成期货（卖出 Call + 买入 Put），同时在 Binance 买入永续合约。",[119,4578,4579,4582],{},[129,4580,4581],{},"反转套利（Reversal \u002F BF+SS）","：当合成期货价格 \u003C Binance 期货价格时，在 Deribit 买入合成期货（买入 Call + 卖出 Put），同时在 Binance 卖出永续合约。",[119,4584,4585],{},"本质上就是\"哪边便宜哪边买，哪边贵了哪边卖\"。",[110,4587,4589],{"id":4588},"对冲机制为什么涨跌都不怕","对冲机制：为什么涨跌都不怕",[119,4591,4592],{},"币安端永远做与 Deribit 完全相反的动作。Deribit 开合成多头，Binance 必开永续空头。BTC 暴涨暴跌时，两端的盈亏完全抵消，行情风险归零。",[119,4594,4595],{},"利润不来自方向判断，而是开仓瞬间两端的定价差。期权到期结算时，合成价格强制回归现货价，基差收敛完成无风险落袋。",[110,4597,4599],{"id":4598},"一个关键区分行权价-现价","一个关键区分：行权价 ≠ 现价",[119,4601,4602,4603,4606],{},"看板中的合约代码 ",[62,4604,4605],{},"BTC-10JUN26-62000","，62000 是行权价，不是当时的市场现价。当时 Binance 的标记价约为 62,991 美元。因为现价高于行权价，看涨期权呈浮盈状态，看跌期权的权利金已被吃满。",[119,4608,4609,4610,238],{},"比对套利机会时，标的不是行权价本身，而是",[129,4611,4612],{},"合成总成本 = 行权价 + 买Call权利金 - 卖Put权利金",[110,4614,4615],{"id":4615},"完整数值演练",[119,4617,4618],{},"设定场景：",[123,4620,4621,4624,4627],{},[126,4622,4623],{},"Binance 现价：$63,000",[126,4625,4626],{},"Deribit 行权价 $62,000 的期权：Call 权利金 $1,500，Put 权利金 $300",[126,4628,4629,4630],{},"合成空头总成本：$62,000 + $1,500 - $300 = ",[129,4631,4632],{},"$63,200",[119,4634,4635],{},[129,4636,4637],{},"开仓动作：",[123,4639,4640,4643],{},[126,4641,4642],{},"Binance 开多，$63,000",[126,4644,4645],{},"Deribit 合成空头（卖 Call + 买 Put），$63,200",[119,4647,4648,4649],{},"开仓瞬间锁定利润：$63,200 - $63,000 = ",[129,4650,4651],{},"$200",[119,4653,4654],{},[129,4655,4656],{},"极端行情 A - 暴涨至 $80,000：",[123,4658,4659,4662,4665],{},[126,4660,4661],{},"Binance 多头盈利：+$17,000",[126,4663,4664],{},"Deribit 合成空头亏损：-$16,800",[126,4666,4667],{},"合计：**+$200**",[119,4669,4670],{},[129,4671,4672],{},"极端行情 B - 暴跌至 $40,000：",[123,4674,4675,4678,4681],{},[126,4676,4677],{},"Binance 多头亏损：-$23,000",[126,4679,4680],{},"Deribit 合成空头盈利：+$23,200",[126,4682,4667],{},[119,4684,4685],{},"无论涨跌，总利润始终为开仓时锁定的 $200。到期自动结算收敛，整个过程是左手倒右手，利润在开仓那一下就已经确定了。",[110,4687,4689],{"id":4688},"模型不难难的是工程","模型不难，难的是工程",[119,4691,4692],{},"这套套利的交易系统作者说过一句话：套利模型倒是不难，难的是一些工程上的事情。",[119,4694,4695],{},"确实，逻辑上这就是一个低买高卖的价差捕捉，但实际跑起来要处理的问题远比模型复杂：两个交易所的 API 延迟差异、订单簿深度和滑点、手续费摩擦对利润空间的侵蚀、资金在两个平台之间的划转效率、期权到期时的自动结算对齐、极端行情下的风控（硬止损、Gamma 检查、行情降级）、以及资金容量上限——能套的价差就那么多，资金量大了反而吃不掉。这些工程细节才是决定系统能不能真正赚钱的关键。",{"title":362,"searchDepth":537,"depth":537,"links":4697},[4698,4699,4700,4701,4702,4703,4704],{"id":4536,"depth":537,"text":4537},{"id":4546,"depth":537,"text":4547},{"id":4567,"depth":537,"text":4567},{"id":4588,"depth":537,"text":4589},{"id":4598,"depth":537,"text":4599},{"id":4615,"depth":537,"text":4615},{"id":4688,"depth":537,"text":4689},"看到一套量化套利系统后追问AI，拆解Deribit合成期货与Binance永续合约之间的跨平台套利逻辑，含完整数值演练。",{},"\u002Fwriting\u002F2026-07-26-btc-synthetic-futures-arbitrage","2026-07-26T13:37:11+08:00",{"title":4528,"description":4705},"btc-synthetic-futures-arbitrage","writing\u002F2026-07-26-btc-synthetic-futures-arbitrage",[4713,4714,4715,4716,4717,4718],"BTC","期权","套利","合成期货","Deribit","Binance","o_rfqacyzAeRyNmp-ceWG6OvHBlEmjo5vFyZhRVC06A",{"id":4721,"title":4722,"body":4723,"category":558,"date":4345,"description":4888,"extension":561,"hidden":562,"meta":4889,"navigation":31,"path":4890,"publishedAt":4891,"seo":4892,"slug":4893,"stem":4894,"tags":4895,"updated":578,"__hash__":4899},"writing\u002Fwriting\u002F2026-07-26-why-vector-database.md","向量库到底有什么用",{"type":107,"value":4724,"toc":4881},[4725,4729,4732,4743,4746,4760,4764,4767,4781,4784,4788,4791,4794,4814,4821,4824,4827,4830,4848,4851,4854,4878],[110,4726,4728],{"id":4727},"sql-存个相同列-join-一下不就行了","SQL 存个相同列 join 一下不就行了？",[119,4730,4731],{},"这是我最初的问题。看起来很合理：embedding 不就是一串数字吗，存个字段，SQL join 一下就完事了。",[119,4733,4734,4735,4738,4739,4742],{},"但 SQL 擅长的是 ",[62,4736,4737],{},"WHERE A = B"," 这种精确匹配。Embedding 是高维浮点数组，代表语义特征。两句话语义相似，它们的向量数值却绝不相等。",[62,4740,4741],{},"WHERE Va = Vb"," 直接返回空。",[119,4744,4745],{},"所以向量库真正在做的事情有两层：",[277,4747,4748,4754],{},[126,4749,4750,4753],{},[129,4751,4752],{},"数学计算","：用余弦相似度等算法算高维空间的\"距离\"，而不是判等。",[126,4755,4756,4759],{},[129,4757,4758],{},"专属索引（ANN 算法）","：传统 B-Tree 索引对高维数据失效，会退化成全表扫描。向量库用 HNSW 之类的索引在高维空间建\"地图\"，做到毫秒级近似最近邻检索。",[110,4761,4763],{"id":4762},"那-rag-到底是怎么跑的","那 RAG 到底是怎么跑的",[119,4765,4766],{},"搞清楚向量库的作用之后，整个 RAG 链路就顺了：",[277,4768,4769,4772,4775,4778],{},[126,4770,4771],{},"用户提问 -> 经过 Embedding 模型 -> 转成向量",[126,4773,4774],{},"拿向量去向量库里找最近的文档片段",[126,4776,4777],{},"找到的相关资料 + 用户原问题一起拼成 Prompt 发给大模型",[126,4779,4780],{},"大模型结合资料回答，减少幻觉",[119,4782,4783],{},"核心就是：输入给大模型之前，先用向量检索把相关上下文捞回来，拼在一起再喂进去。",[110,4785,4787],{"id":4786},"全量召回还是选几条","全量召回还是选几条？",[119,4789,4790],{},"这是第二个直觉问题：如果数据库里相关的东西很多，比如 50 页文本都沾边，全部塞进大模型？还是挑几条最相关的？",[119,4792,4793],{},"答案是必须精选截断。原因有三个：",[123,4795,4796,4802,4808],{},[126,4797,4798,4801],{},[129,4799,4800],{},"上下文窗口有限","：大模型的 Context Window 有上限，塞不下就是塞不下。",[126,4803,4804,4807],{},[129,4805,4806],{},"中间迷失","：上下文太长时，大模型容易忽略中间的内容。",[126,4809,4810,4813],{},[129,4811,4812],{},"Token 费用","：输入越长越贵。",[119,4815,4816,4817,4820],{},"所以标准做法是设置 ",[62,4818,4819],{},"top_k","（比如 limit=5），只取相似度最高的前 N 个片段。入库之前也先把长文本切成小块（比如每段 500 字），精准匹配局部内容，避免带入无关段落。",[110,4822,4823],{"id":4823},"但我的数据量没那么大",[119,4825,4826],{},"说到这里我反应过来了--我的复盘日志根本没多少数据。几万字而已，全塞进去不就完了？",[119,4828,4829],{},"确实可以。小数据量下\"全上下文注入\"更简单有效：",[123,4831,4832,4838],{},[126,4833,4834,4837],{},[129,4835,4836],{},"逻辑完整","：保留完整的时间线和因果链，不会因为切片断章取义。",[126,4839,4840,4843,4844,4847],{},[129,4841,4842],{},"架构极简","：直接 SQL ",[62,4845,4846],{},"SELECT *","，全部喂给大模型，不需要向量索引和切片逻辑。",[110,4849,4850],{"id":4850},"那向量库还有没有意义",[119,4852,4853],{},"有。即便是小数据量，向量库（或 SQL 的向量插件）仍然有几个不可替代的能力：",[123,4855,4856,4866,4872],{},[126,4857,4858,4861,4862,4865],{},[129,4859,4860],{},"模糊语义匹配","：日志里写的是\"肝到两点\"或\"披星戴月\"，向量检索能匹配到\"加班\"的语义。传统 SQL ",[62,4863,4864],{},"LIKE"," 搜不到这种。",[126,4867,4868,4871],{},[129,4869,4870],{},"扩展性","：日志会越积越多，一两年后可能就塞不进上下文窗口了。向量检索的成本随数据量亚线性增长，全量注入则是线性上升。",[126,4873,4874,4877],{},[129,4875,4876],{},"聚类与挖掘","：基于向量距离可以做无标签的主题归类和情绪分析，这是全量注入给大模型做不到的。",[119,4879,4880],{},"所以比较务实的方案是：短周期（比如近一周）的分析直接 SQL 全量提取，喂给大模型；跨年度的特定问题搜索走向量匹配。两条路各管各的，不冲突。",{"title":362,"searchDepth":537,"depth":537,"links":4882},[4883,4884,4885,4886,4887],{"id":4727,"depth":537,"text":4728},{"id":4762,"depth":537,"text":4763},{"id":4786,"depth":537,"text":4787},{"id":4823,"depth":537,"text":4823},{"id":4850,"depth":537,"text":4850},"从\"SQL 加个列不就行了\"到理解向量检索和 RAG 的完整链路，以及小数据量场景下到底该怎么选。",{},"\u002Fwriting\u002F2026-07-26-why-vector-database","2026-07-26T00:09:14+08:00",{"title":4722,"description":4888},"why-vector-database","writing\u002F2026-07-26-why-vector-database",[4896,4897,4898],"向量数据库","Embedding","RAG","hdqIEfJHF7KE9cYD971ShCFoCR9FbJVBIvdlCmdrbf4",1785763813110]