[{"content":" Single pawn # The King should be in front of his Pawn, with at least one intervening square. Advance the King as far as possible with the safety of the pawn, and never to advance the Pawn until it is essential to its own safety. Other Pawn Endings # ℹ️ Don\u0026#39;t push pawns prematurely don\u0026rsquo;t push the pawn prematurely, which may send it to the opponent\u0026rsquo;s King, and you can\u0026rsquo;t protect it. In this position, if white pushes the f-pawn, the game draws because ... Kf7. White is better to move his King and force the Black king to the corner, with help from g6 push, controlling the f7 square .\n(FEN: 4k3/6p1/4K3/5PP1/8/8/8/8 w - - 1 1 ) Two pawns vs one # In this example, white cannot push the pawn, which results in exchange and draw. He need to first drive the Black king out of the way, before pushing his f pawn.\nEmbedded chessboard from lichess.org\nAnother example of 2v1 in the above study.(see 2v1#2 in the same study) White should first move the King closer to the pawn, before pushing. Otherwise, Black can push and force pawn trades, then Black\u0026rsquo;s King is in a good position to draw.\nThree against two # 💡 Advance pawn with no opposition Whenever there\u0026rsquo;s no reason against it, advance the pawn that has no Pawn opposition it in pawn end games. In this example, push any pawn wins, but push the f pawn follows the principle.\nEmbedded chessboard from lichess.org\nℹ️ Act immediately on the side where you have superior forces Here, White\u0026rsquo;s idea is to first act on the King side, pushing both pawns deep, locking Black\u0026rsquo;s King to defend on King\u0026rsquo;s side. Then, White\u0026rsquo;s King is free to go to Queen side and win there.\nEmbedded chessboard from lichess.org\n","date":"16 一月 2021","externalUrl":null,"permalink":"/posts/chess-fundamentals/first-principles/","section":"Posts","summary":"Single pawn # The King should be in front of his Pawn, with at least one intervening square. Advance the King as far as possible with the safety of the pawn, and never to advance the Pawn until it is essential to its own safety. Other Pawn Endings # ℹ️ Don't push pawns prematurely don’t push the pawn prematurely, which may send it to the opponent’s King, and you can’t protect it. In this position, if white pushes the f-pawn, the game draws because ... Kf7. White is better to move his King and force the Black king to the corner, with help from g6 push, controlling the f7 square .\n","title":"1. First Principles: Endings, Middle-game and Openings","type":"posts"},{"content":"Imbalances: the static and dynamic differences.\nList of imbalances # Minor pieces: the interplay between Bishops and Knights Pawn structure: doubled/isolated/backward/passed pawns, islands. Space Material Files and squares: files, ranks and diagonals act as pathway, while squares act as homes. Development: this is a temporary balance. Initiative: also temporary. ⚠️ Don\u0026#39;t look at individual moves! Try to understand the position, before starting calculation on individual moves. Bishop vs Knights # It\u0026rsquo;s the most important imbalance of the game.\nBasic rules of minor pieces # Both worth 3 points. It\u0026rsquo;s up to you to manipulate the position to make your piece more valuable. Bishops are best in open position, where pawns don\u0026rsquo;t block their diagonals. Bishops are strong in end games where both sides have passed pawns: the long range ability to control. \u0026ldquo;Bad\u0026rdquo; bishop is the one having same color with your center pawn. However bad Bishop can still be active. Bishop\u0026rsquo;s weakness is it can only control one color. A pair of bishops is very strong. Knights love closed positions and locked pawns. The ability to jump over other pieces make them valuable. Knights are usually better in the center. Knights needs secure, advanced homes to be effective. (6th rank is the best!) Knights are superior in end game if pawns are on one side. Best way to fight Knights is to remove their advanced support points. Example games # Silman-Gross: Quiet position # In this position, the imbalance is Bishop vs Knight. So, white\u0026rsquo;s plans are:\nLimiting black\u0026rsquo;s Knight\u0026rsquo;s advance points, by b-pawn pushes. Moving his Bishop to a better diagonal(e3). Embedded chessboard from lichess.org\nThe key here is to understand the position, stick to the plan(improving the pieces), and not get distracted.\nℹ️ Tips on Bishop vs Knight If you have Bishop, try limit advanced support points of Knight. If you have Knight, try to create advanced homes for it. Understand the imbalance, then make the position more valuable to your side. Don\u0026rsquo;t focus on individual moves before having a plan. Fisher-Taimanov: Act quick # In this position, the imbalances are: 1. Bishop vs Knight and 2.Pawn majority on Queen side\nBlack is attacking h4. But if we focus on imbalance, we\u0026rsquo;ll see that, Black\u0026rsquo;s Knight has a perfect home at c5, where:\nIt\u0026rsquo;s a hole, and a light square, so White\u0026rsquo;s dark bishop has no way to kick it away. It blocks white\u0026rsquo;s pawns on Queen side(and attacks them), making his pawn majority useless. With the vision of such imbalance, White\u0026rsquo;s priority is to stop the Knight, use his queen side majority. White\u0026rsquo;s idea is to create a passed pawn.(Majority, and Black\u0026rsquo;s pawn is pinned) Sacrificing h4 on King\u0026rsquo;s side is not big deal: white\u0026rsquo;s Queen\u0026rsquo;s side can win him this game.\nALl the imbalances shows white should act on queen side, instead of defending h4:\nKnight vs Bishop: Stop Knight from getting to c5. Knight vs Bishop: White\u0026rsquo;s Bishop is supporting Queen side\u0026rsquo;s pawn push. Pawn majority: White has pawn majority on Queen side, and there\u0026rsquo;s chance to attack. ℹ️ Identify your strength and act quick Find your strength by looking at the imbalance, and use it before it goes away. Sometimes, that means act immediately, even sacrificing on other parts. Also, find your opponent\u0026rsquo;s key plan by checking imbalances, and stop it. Rooks belong behind passed pawns! Embedded chessboard from lichess.org\nThe game between C players # This game has lots of doubious moves, but is a good lesson on imbalance, and how not to play.\nEmbedded chessboard from lichess.org\nChoice: Close position or take away Knight\u0026rsquo;s square # No rule is correct all the time. For example, in a BvN game, a move that takes away Knight\u0026rsquo;s square may also close the position. What do you do?\nChess.com lesson\nCorrect play is NOT to kick the Knight: it closes the position and dimnishes the Bishops. Black should open us the position instead, by f7-f6 to support e6-e5.\nThe game with annotation: We can see that, once the position is open, White\u0026rsquo;s Knight becomes a weak point, requiring defending, while Black\u0026rsquo;s Bishops+Rooks are all activated. Same position, Silman v 1700, with incorrect play(kicking the Knight): The 1700 student is too obsessed with checkmate threats, instead of trying to slowly improve the position.\nℹ️ Take away from 1700 game Always assume your opponent will play the best response. If you have pair of bishops, opening up the position is more important than kicking the Knight. Limiting the Knight when you have a Bishop pair # In the Hort\u0026rsquo;s game, white has pair of bishop. Black\u0026rsquo;s Knight has no chance on the Queenside, and white follows up with g5 and g4 to lock the King side.\nLimiting the Knight\n(FEN: 4n3/1b3k1p/p2p2p1/3Pp3/2P3P1/6PP/2BB4/6K1 w - - 0 1) Which piece to keep? # In this position, black has 3 choices:\n1... Rc5 for a R+N v R+B endgame 1... Nxe4 for a pure Rook endgame 1... Rd4 for a Knight vs Bishop endgame ℹ️ Bishop are better in pawn race endings Embedded chessboard from lichess.org\n","date":"15 一月 2021","externalUrl":null,"permalink":"/posts/amateurs-mind/imbalances/","section":"Posts","summary":"Imbalances: the static and dynamic differences.\nList of imbalances # Minor pieces: the interplay between Bishops and Knights Pawn structure: doubled/isolated/backward/passed pawns, islands. Space Material Files and squares: files, ranks and diagonals act as pathway, while squares act as homes. Development: this is a temporary balance. Initiative: also temporary. ⚠️ Don't look at individual moves! Try to understand the position, before starting calculation on individual moves. Bishop vs Knights # It’s the most important imbalance of the game.\n","title":"Imbalances and Bishop vs Knight","type":"posts"},{"content":"Amateurs often have no clue of how to use center/space advantage, instead keen on forcing continuations, and unjustified attacks.\nRules of center # Rule 1. Full pawn center gives control and space. # A full pawn center gives its owner territory and control over key central squares.\nRule 2. Owning a center is responsibility. # Owning a full pawn center is a responsibility. You must make it indestructible, and you will cramp your opponent.\nRule 3. Don\u0026rsquo;t advance center too early. # Don\u0026rsquo;t advance the center too early! Every pawn move weakens some squares.\nIn the following example, advancing e-pawn allows the Knight jump to d5 and f5.\nAdvancing the e-pawn weakens d5 and f5\n(FEN: 8/4n3/8/8/3PPP2/2P3P1/8/8) Rule 4. Attack your opponent\u0026rsquo;s full pawn center. # One of the most common cases of allowing a \u0026ldquo;string\u0026rdquo; center in order to attack it is in the Alekhine\u0026rsquo;s Defense.\nEmbedded chessboard from lichess.org\nRule 5. If center pawn get traded, the open file is good for Rooks to use. # In this example, the open e-file is nice for a Rook.\n1. e4 e6 2. d4 d5 3. exd5 exd5 Open center\n(FEN: rnbqkbnr/ppp2ppp/8/3p4/3P4/8/PPP2PPP/RNBQKBNR w KQkq - 0 4) Rule 6. If the center gets locked, then the play switches to wings. # In this example, it\u0026rsquo;s easy to see that the center is a dead zone, and play should switch the wings.\n1. d4 Nf6 2. c4 c5 3. d5 e5 4. Nc3 d6 5. e4\nLocked center\n(FEN: rnbqkb1r/pp3ppp/3p1n2/2pPp3/2P1P3/2N5/PP3PPP/R1BQKBNR b KQkq - 0 5) Rule 7. With a locked center, play on the wing where your pawns point to. # The pawns point to the area where you have more space, and that\u0026rsquo;s where you want to control.\nIn the diagram below, Black\u0026rsquo;s pawn points to kingside. (c7-d6-e5). While whit\u0026rsquo;s pawns points to both sides.\nℹ️ In general, push the pawn that stands next to your most advanced pawn For black, push \u0026hellip;f7-f5 next to the most advanced e5 pawn.(gaining space and opening a file for the Rook).\nFor white, he should push a pawn to side-byside with his d5 pawn. c4-c5 is good: gaining space on queenside, and prepare to rip open files. Pushing f2-f4 would be bad, after \u0026hellip;exf4, Black is very active, and e-pawn would be backward.\nBlack\u0026#39;s pawn points to kingside, White\u0026#39;s pawn points both sides.\n(FEN: r1bq1rk1/pppnnpbp/3p2p1/3Pp3/2P1P3/2NN4/PP2BPPP/R1BQ1RK1) Rule 8. An open center allows you attack with pieces. A closed center means you must attack with pawns. # Rules of space # When you have more spcae, your pieces are easier to move/coordinate. However, as you advance pawns, some squares becomes weak. It\u0026rsquo;s important to not over-extend too early.\nRule 1. Avoid exchanges when you have more space. # In the example below, black\u0026rsquo;s pieces are cramped and cannot even move. However, if we exchange and remove the pieces and only leave pawns, black would be perfectly fine.\nCramped up with no where to go\n(FEN: rnb2qr1/2p1nkb1/ppPp1p1p/3PpPp1/PP2P1PP/2NN2Q1/4BK1R/2B4R) Rule 2. If you have less space, try to exchange to get room for your pieces. # Rule 3. A spatial plus is permanent, long-term advantage. Don\u0026rsquo;t rush to use it. # Games # 1750-Silman: passive White play not utilizing center/space. # In this game, white(1750) has better space, but he was playing too passive, reacting to imaginary one-move threats with no plan on his own.\nℹ️ Tips from the game Passive planless play leads to loss. Play on the center \u0026gt; wings. Know what the opponent is planning, but don\u0026rsquo;t allow yourself become mesmerized by his ideas. Your plans should prove stronger than theirs. Don\u0026rsquo;t make pointless one-move attacks!!! Always expect your opponent to see your threats. Embedded chessboard from lichess.org\n2000-Silman: Modern defense: bad pawn push # In this game, White has a strong center, but played aimless and careless, opening up the position while giving Black the bishop pair, costing him the game.\nℹ️ Tips Have a plan before your development, so you know where to best put your pieces. Carefully analyze the position, instead of checking random moves. Embedded chessboard from lichess.org\n1600-Silman: Modern defense 2. # ℹ️ Tips Don\u0026rsquo;t think moving (pawns) forward is good. It weakens squares. Games are often won by taking space and restricting opponent. You don\u0026rsquo;t need to attack like crazy to win. The side with space advantage should avoid trading, while side with less space should seek exchanges. Fischer-Gheorghiu, 1970 # White takes control of the center, limiting Black\u0026rsquo;s pieces with no good forward moves, slowly improving position of his own pieces and eventually won.\nEmbedded chessboard from lichess.org\nReti-Carls: King-side space. # Reti won the position by: avoiding unnecessary exchanges, then making use of h-file and open whenever he wants.\nDue to the space advantage, White can double/triple Rook/Queen on h-file, while black can\u0026rsquo;t do the same. Once white has enough pressure, he opens the h-file and breaks through. This example shows how to use space advantage.\nEmbedded chessboard from lichess.org\n1700-Silman: White wrongly closes the h-file and lose. # In the same position, the student closes the h-file on the first move. (engine evaluation jumped from +2.x points to 0).\nℹ️ Space advantage means nothing if position is **closed** Silman-Barkan, 1981: how White gains space everywhere. # In this example, White already has space advantage on Queenside. However, he sees the chance to gain space on center and King side, and did so by some excellent moves. Once he has the space advantage everywhere, he can improve his pieces, move then to strike hard with greater mobility.\n1500-Silman: He failed to stop Black\u0026rsquo;s counterplay at center, allowed black to gain space on King side and storm there, allowing the game becomes a race. 1650-Silman: Identified white should stop Black\u0026rsquo;s plan to break at center. ","date":"17 一月 2021","externalUrl":null,"permalink":"/posts/amateurs-mind/center-and-space/","section":"Posts","summary":"Amateurs often have no clue of how to use center/space advantage, instead keen on forcing continuations, and unjustified attacks.\nRules of center # Rule 1. Full pawn center gives control and space. # A full pawn center gives its owner territory and control over key central squares.\n","title":"The Center, Territory and Space","type":"posts"},{"content":"Knowning which pawn structure is \u0026ldquo;weak\u0026rdquo; is not enough: one need to know how to attack them, or making use of the weaknesses.\nIn general, the formular to attack weak pawns(isolated, backward) is to control the square in front of the pawn so it can\u0026rsquo;t advance. (the squares are usually weak since no pawn can defend it). Then pile up on the weakness.\nEvery structure has its pros and cons.\nGeneral Rules # Double pawns # ✅ Positive Extra open files, and increased square control.(especially on center files.) ❌ Negative Reduced flexibility, and vulnerable to attack. (the leading pawn is usually weaker) Example: In the following position, white is happy for a Bishop trade and double the pawn on e-file. Black should play Bb6, welcoming exchange and open a-file as well.\nAfter Be3, white invities Black to trade to open the f-file and double pawns on e-file.\n(FEN: r1bqk2r/ppp2ppp/2np1n2/2b1p3/2B1P3/2NPBN2/PPP2PPP/R2Q1RK1 b) Isolated Pawns # ✅ Positive The creation of such pawn provides half-open file to use. Central isolated pawns provide central control and open file for Rooks. Owner of such pawns should play dynamically. ❌ Negative Cannot be protected by other pawns, vulnerable if on an open file. The formular to attack isolated pawns is to fixate it, by controlling the weak square in front of it. Trade all minor pieces, pile up Rooks and Queens on it to pin the pawn, then attack with a friendly pawn. Example on how to attack isolated pawn with R+Q Black can win the fixed and isolated d-pawn by 1...e5.\n(FEN: 6k1/p2q1p2/1p2p1p1/3r3p/3P3P/P1Q3P1/1P3P2/3R2K1 b) If you have an isolated pawn, try to exchange Rooks! Then the enemey won\u0026rsquo;t have enough forces to win the pawn.\nBackward pawns # ✅ Positive It guards other pawns. The advanced pawn can block enemy pieces and control squares. The backward pawn is not weak if the square in front of it is well-defended. ❌ Negative It\u0026rsquo;s weak if it\u0026rsquo;s sitting on a open file and cannot advance. Attack should control the square directly in front of the pawn, turning the pawn into an immobile target. One way is to exchange its defenders. Example: Square in front of the backward pawn as well as the pawn is well-defended, so the pawn is not a weakness.\nWhite to play and has nothing. d6 is not weak because d5 is controlled well by Black.\n(FEN: 2rr2k1/1bqnbppp/p2p1n2/1p2p3/4P3/1PN2N2/PBPQ1PPP/2RR1BK1 w) Hanging pawns # Haning pawns is a pair of pawns on same rank on c/d files(or e-f files).\nIt can be weak if pawns are immobile and attacked. It can be strong since it provides control of central squares and space.\nThe side has hainging pawns should play dynamically and protect them.\nPassed pawns # A passed pawn is very strong if the owner has play elsewhere: it can be used as endgame insurance.\nThe square in front of the passed pawn is the most important square on the board. Whoever controls the square dominates the game.\nGames # 1800-Silman: Black uses hanging pawns for dynamic potential. # ℹ️ Tips Any kind of weak pawn must first be contained(blocked) before it\u0026rsquo;s attacked Don\u0026rsquo;t just react to opponent\u0026rsquo;s plans. Find an idea and follow it yourself. Embedded chessboard from lichess.org\nTriple pawn gives victory # In this example, White has triple pawn but it gives control of squares, also provides spaces and open files that white can use.\nEmbedded chessboard from lichess.org\nBackward pawn: fight for square in front of it! # In this example, black has a backward pawn on d6, and he should fight for d5(in front of the pawn). However, he traded away his light square bishop, losing an important defender of that square, and allowed white to dominate d5.\nℹ️ Tips The weak square in front of a backward pawn is often a greater problem than the pawn itself. You can play to win a square by trading off its defenders. Embedded chessboard from lichess.org\n1650-Silman: Pawn structure and close/open position choice. # In this example, white has choice to close or open the position, and he closed it incorrectly, just to kick the black knight (to a not-so-bad position). Then with center closed, white should play on the wing, but he didn\u0026rsquo;t.\nℹ️ Tips In closed positions you play where your pawns point to. (less important in open positions) Identify a target, build up your pieces to attack it. Don\u0026rsquo;t just react to your opponent\u0026rsquo;s plan. Don\u0026rsquo;t attack something for no reason. Your opponent always sees it. Only attack if the resulted position is better for you. Embedded chessboard from lichess.org\n","date":"24 一月 2021","externalUrl":null,"permalink":"/posts/amateurs-mind/pawns/","section":"Posts","summary":"Knowning which pawn structure is “weak” is not enough: one need to know how to attack them, or making use of the weaknesses.\nIn general, the formular to attack weak pawns(isolated, backward) is to control the square in front of the pawn so it can’t advance. (the squares are usually weak since no pawn can defend it). Then pile up on the weakness.\nEvery structure has its pros and cons.\n","title":"Pawn Structure","type":"posts"},{"content":" Lichess study of this chapter # Embedded chessboard from lichess.org\nThe initiative # White has the move, which means initiative. He tries to first control the center, or obtain some positional advantage, so it\u0026rsquo;s possible for him to keep on harrassing the enemy.\nHe only relinquishes the initiative when he gets for some material advantage in return.\nDiract attack en masse # ℹ️ Attack en masse Attacks must be carried on with sufficient force to gurantee its success. Threatend attack # When there\u0026rsquo;s no change to directly attack the king, we can demonstrate something on one side, drawing opponent force there, then use greater mobility of our force to switch to the other side and break through.\nSee Capablanca vs Blanco, 1913\nCutting pieces off the scene of action # Sometimes we can cut a piece (usually Bishop or Knight) from the scene of actual conflict. See Winter vs Capablanca, 1919.\n","date":"16 一月 2021","externalUrl":null,"permalink":"/posts/chess-fundamentals/general-theory/","section":"Posts","summary":"Lichess study of this chapter # Embedded chessboard from lichess.org\nThe initiative # White has the move, which means initiative. He tries to first control the center, or obtain some positional advantage, so it’s possible for him to keep on harrassing the enemy.\nHe only relinquishes the initiative when he gets for some material advantage in return.\n","title":"4. General Theory","type":"posts"},{"content":" Lichess study of this chapter # Embedded chessboard from lichess.org\nThe sudden attack from a different side # Similar to general theory, the idea is to first attacking on one side, then, with greater mobility of the pieces, quickly transfer attack to the other side and break through, before the opponent can respond.\nRooks and pawns endings # The rooks and pawns endings are difficult. Usually, the idea is to forcing opponent to defend a weak point, removing their mobility, and use our own greater mobility to switch side and attack.\n","date":"16 一月 2021","externalUrl":null,"permalink":"/posts/chess-fundamentals/end-game-strategy/","section":"Posts","summary":"Lichess study of this chapter # Embedded chessboard from lichess.org\nThe sudden attack from a different side # Similar to general theory, the idea is to first attacking on one side, then, with greater mobility of the pieces, quickly transfer attack to the other side and break through, before the opponent can respond.\n","title":"5. End Game Strategy","type":"posts"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/categories/coding/","section":"Categories","summary":"","title":"Coding","type":"categories"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/tags/fzf-lua/","section":"Tags","summary":"","title":"Fzf-Lua","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/tags/neovim/","section":"Tags","summary":"","title":"Neovim","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/tags/treesitter/","section":"Tags","summary":"","title":"Treesitter","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/tags/vim/","section":"Tags","summary":"","title":"Vim","type":"tags"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/","section":"Yang's Notes","summary":"","title":"Yang's Notes","type":"page"},{"content":"","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/posts/","section":"文章","summary":"","title":"文章","type":"posts"},{"content":"太久没写代码，重拾 Neovim 时发现不仅肌肉记忆有些生疏，而且因为插件生态的“疯狂内卷”，原来的配置报出了一连串的错误。于是和 AI 结结实实地“折腾”了一番，完成了一次 Neovim 架构的现代化升级。\n这篇博客是我和 AI 聊天复盘的摘要记录，方便以后如果又断档了，可以快速接上思路。\n1. 认知转换：从“极客生存手册”到“现代 IDE” # 在重新梳理快捷键和工作流时，AI 敏锐地指出我之前的旧文档思维还停留在原生 Vim 时代。而我现在的配置，其实已经是一个极其现代化的 IDE，工具其实在主动配合直觉：\n搜索与查找：从敲命令退化到了全屏互动（全面拥抱 fzf-lua，支持代码预览和悬浮窗）。 精准跳转：不再依赖盲按 j/k 或者原生的标记，而是依靠视觉反馈的 Leap (s 瞬移) 和 Flash (S 语法树节点选择)。 多文件管理：用 Grapple (\u0026lt;leader\u0026gt;ma 打标, \u0026lt;leader\u0026gt;n 轮切) 替代了需要心智负担的数字 Buffer 管理，只把核心文件钉在书签里。 大纲与排错：用 Trouble 和 Aerial 侧边栏替代了底部的纯文本 Quickfix 列表。 2. 踩坑纪实：Treesitter 0.10+ 断代大迁移 # 这次遇到了 Neovim 社区目前最大的一个“断层更新”巨坑。因为我使用了 Neovim 0.12.0，核心 API 的变化导致了连环爆炸：\nattempt to call method 'range' (a nil value) 原因：Neovim 核心废弃了旧的 range API，而旧版 (master 分支) 的 nvim-treesitter 还没跟上，导致搜索 Markdown 或打开历史文件时崩溃。 解决：将 nvim-treesitter 及其相关生态插件全面切换到新的 main 分支。 module 'nvim-treesitter.configs' not found 原因：切换到 main 分支后，官方进行了架构重构，直接删除了内置的 configs 模块。 解决：将 textobjects 等子插件从原先的嵌套配置中剥离，使用独立的 setup() 调用。 ; 和 , 重复跳转失效 原因：repeatable_move 模块路径变更，且劫持原生 f / F / t / T 的函数必须使用 _expr 后缀并开启表达式计算。 解决：重写 keymaps.set，使用 builtin_f_expr 搭配 { expr = true } 完美复活了原有的肌肉记忆。 底层的 GLIBC 版本冲突 原因：main 分支不再自带解析器编译器，需要系统安装 tree-sitter CLI。但通过 npm 安装的预编译版本极其激进，要求 GLIBC_2.39，与本地 Ubuntu 22.04 环境（GLIBC 2.35）不兼容而直接崩溃。 解决：绕过包管理器，直接下载 GitHub 上的静态编译原生二进制文件 tree-sitter-linux-x64 放入本地 PATH 中，完美修复。 3. 体验跃升：代码折叠与终极面包屑导航 # 在解决了报错之后，我们还顺手升级了两项极大地提升编码幸福感的功能：\n开启原生 Treesitter 代码折叠 # 彻底抛弃了厚重的第三方折叠插件，直接启用了 Neovim 0.10+ 基于 C 语言底层原生实现的代码折叠。\n配置：vim.o.foldmethod = \u0026quot;expr\u0026quot; 和 vim.o.foldexpr = \u0026quot;v:lua.vim.treesitter.foldexpr()\u0026quot; 体验：使用 za (切换)、zM (全折叠)、zR (全展开) 可以基于语法语义（如类、方法块）极其精准地折叠代码，彻底告别以前靠缩进乱折叠的时代。 引入 Dropbar.nvim：降维打击的面包屑 # 彻底移除了停更且严重挤占底部 Lualine 状态栏横向空间的 nvim-navic，换上了纯键盘党的最爱 dropbar.nvim。\n它在窗口顶部安静且优雅地显示 文件 \u0026gt; 类 \u0026gt; 方法。 按下 \u0026lt;leader\u0026gt;xd，顶部面包屑的各个层级会像 Leap 一样亮起字母提示标签。 敲击对应的字母，就能直接弹出该层级的下拉菜单，支持 j/k 上下移动，更支持直接按 / 模糊搜索变量或函数！配合 \u0026lt;leader\u0026gt;ds（Telescope 文档符号搜索），长文件内空降简直无敌。 4. 插件的“折腾税” # 最后，我们还顺手修复了 render-markdown 插件因为作者规范化 API（把旧的 RenderMarkdownToggle 强行改成了子命令格式的 RenderMarkdown toggle）导致的失效问题。\n在这个疯狂内卷的 Neovim 开源生态里，享受极致性能和现代 IDE 体验的代价，就是偶尔需要支付这样的“折腾税”。不过好在熬过阵痛期后，手里的工具确实变得越来越趁手了。有了这篇摘要和整理好的生存指南，下次再拿起来应该就不慌了！\n","date":"2026年4月6日","externalUrl":null,"permalink":"/zh/posts/coding/neovim-chat-summary-2026/","section":"文章","summary":"太久没写代码，重拾 Neovim 时发现不仅肌肉记忆有些生疏，而且因为插件生态的“疯狂内卷”，原来的配置报出了一连串的错误。于是和 AI 结结实实地“折腾”了一番，完成了一次 Neovim 架构的现代化升级。\n这篇博客是我和 AI 聊天复盘的摘要记录，方便以后如果又断档了，可以快速接上思路。\n1. 认知转换：从“极客生存手册”到“现代 IDE” # 在重新梳理快捷键和工作流时，AI 敏锐地指出我之前的旧文档思维还停留在原生 Vim 时代。而我现在的配置，其实已经是一个极其现代化的 IDE，工具其实在主动配合直觉：\n搜索与查找：从敲命令退化到了全屏互动（全面拥抱 fzf-lua，支持代码预览和悬浮窗）。 精准跳转：不再依赖盲按 j/k 或者原生的标记，而是依靠视觉反馈的 Leap (s 瞬移) 和 Flash (S 语法树节点选择)。 多文件管理：用 Grapple (\u003cleader\u003ema 打标, \u003cleader\u003en 轮切) 替代了需要心智负担的数字 Buffer 管理，只把核心文件钉在书签里。 大纲与排错：用 Trouble 和 Aerial 侧边栏替代了底部的纯文本 Quickfix 列表。 2. 踩坑纪实：Treesitter 0.10+ 断代大迁移 # 这次遇到了 Neovim 社区目前最大的一个“断层更新”巨坑。因为我使用了 Neovim 0.12.0，核心 API 的变化导致了连环爆炸：\n","title":"与 AI 的 Neovim 现代化折腾纪实 (2026版)","type":"posts"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/tags/ffmpeg/","section":"Tags","summary":"","title":"Ffmpeg","type":"tags"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/tags/frigate/","section":"Tags","summary":"","title":"Frigate","type":"tags"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/tags/go2rtc/","section":"Tags","summary":"","title":"Go2rtc","type":"tags"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/categories/homelab/","section":"Categories","summary":"","title":"Homelab","type":"categories"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/tags/nvr/","section":"Tags","summary":"","title":"Nvr","type":"tags"},{"content":"","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/tags/self-hosted/","section":"Tags","summary":"","title":"Self-Hosted","type":"tags"},{"content":"每日 LLM 健康检查报告显示 10 台摄像头中有 8 台当天崩溃次数达到数百次。 追查下去，根因是两台婴儿监控摄像头、一个 go2rtc 重连窗口，以及一次 vaapi 级联崩溃——没有一个环节是直接显而易见的。以下是完整的排查与修复过程。\n问题是如何发现的 # 我在搭建一个每日家庭健康代理——一个定时脚本，查询所有家庭服务（Frigate、Home Assistant、Paperless、arr 媒体栈），然后将数据交给本地 LLM 分析。核心思路是：不再手动逐个检查仪表盘，而是每天早上收到一份摘要，自动标出异常项。\nFrigate 的检查项查询 /api/stats，提取每台摄像头的崩溃次数。某天早上，报告返回了这样的数据：\n1 2 3 4 5 6 nanit_adelia: 2228 次崩溃 nanit_leonard: 2228 次崩溃 backyard: 847 次崩溃 front_door: 391 次崩溃 side_a: 203 次崩溃 ... 如果没有健康检查，我根本不会注意到——Frigate 容器本身从未重启，Web UI 上各摄像头仍然显示\u0026quot;在线\u0026quot;，也没有任何告警弹出。\n根因：崩溃级联 # 顺着日志往前追，每天上午 9 点的事件链如下：\nCron 停止 nanit 容器（婴儿监控只需要夜间运行） nanit RTMP 推流断开，go2rtc 失去数据源 go2rtc 在内部保持 nanit RTSP 流存活约 37 分钟（其重连窗口） 约 09:37，go2rtc 放弃，对两路 nanit 流返回 404 Not Found Frigate 的 ffmpeg 在 404 上崩溃；watchdog 立即重启 → 约 10 秒一次的紧密崩溃循环 两路 nanit ffmpeg 进程同时崩溃循环，耗尽 Intel iGPU vaapi 上下文 其他所有摄像头——共用同一 vaapi 设备——崩溃，报错 Failed to sync surface 非 nanit 摄像头在 5–6 小时内逐个自我恢复，随着各自 ffmpeg 进程陆续重启 Frigate 容器本身 0 次重启。所有问题发生在 ffmpeg 子进程层面。崩溃计数是准确的——2228 次大约是 8 小时内每 13 秒一次，与 10 秒的 watchdog 周期完全吻合。\nRTMP 架构背景 # nanit 婴儿监控通过第三方容器（ghcr.io/gregory-m/nanit）与 Nanit 云端认证，并在主机端口 1935 上运行 RTMP 服务端。go2rtc（内嵌于 Frigate）作为客户端从中拉流：\n1 2 3 4 5 6 7 8 9 10 Nanit 摄像头硬件 │ （专有协议 → 云端认证） ▼ nanit 容器 ← RTMP 服务端，监听 :1935 │ ▼ go2rtc ← 拉取 rtmp://10.0.10.11:1935/local/\u0026lt;uid\u0026gt; │ RTSP ▼ Frigate ffmpeg 修复目标：确保 go2rtc 始终有有效的 RTMP 流可以拉取，即使 nanit 容器已停止。\n失败的尝试 # go2rtc ffmpeg: 备用源 # go2rtc 支持为每个流配置多个源——主源失败时自动切备源。方案：添加一个用 ffmpeg 生成黑屏的备用源。\n1 2 3 nanit_adelia: - rtmp://10.0.10.11:1935/local/ec06240f - \u0026#34;ffmpeg:-re -f lavfi -i color=black:size=640x480:rate=1 -c:v libx264 -preset ultrafast {output}\u0026#34; 问题一： Frigate 会将 {...} 预处理为环境变量模板，{output} → Invalid substitution found。用 {{output}} 双花括号转义可绕过。\n问题二： go2rtc 的 ffmpeg: 源不会对参数做 shell 分割，整个字符串作为单一 token 传入。结果：Error opening input file -re.\n{output} 和 #video 简写方式均告失败。\n使用 exec:ffmpeg 加 GO2RTC_ALLOW_ARBITRARY_EXEC=true # go2rtc 有 exec: 源类型，支持运行任意命令。设置环境变量、配置正确语法后，/dev/shm/go2rtc.yaml 中 exec: 行显示正常——但 ffmpeg 进程从未实际启动。无报错，ps aux 中也看不到进程。原因不明，疑似静默启动失败。\n独立占位容器使用不同端口 # 创建 nanit-placeholder 容器，将黑屏帧推送到 go2rtc 的不同路径，再利用 go2rtc 的备用源：主源用 1935，备源用 1936。\n致命缺陷： go2rtc 切到备源后，即使主源恢复也不会自动切回，只有当前会话断开才重新尝试。强制切回需要重启 go2rtc 子进程，会导致所有摄像头中断 2–3 秒。不可接受。\n占位容器推流到 go2rtc RTMP # 占位容器尝试通过 frigate_default Docker bridge 网络推流到 rtmp://frigate:1935/...，结果：Connection refused。\ngo2rtc 的 RTMP 端口只能从 network_mode: host 的容器访问（如真实的 nanit 容器），bridge 网络容器无法访问。原因：go2rtc 的 RTMP 端口绑定在主机网卡上，而非 Docker bridge。\n修复方案：基于 mediamtx 的同端口切换 # 核心洞察：如果占位服务接管同一个端口（1935），go2rtc 根本察觉不到切换。不需要备用源逻辑，不需要重连触发，不影响其他摄像头。\n1 2 3 4 5 6 nanit 容器 ← 19:00–09:00，RTMP 服务端占用主机 :1935 或 nanit-placeholder ← 09:00–19:00，mediamtx RTMP 服务端占用主机 :1935 │ ▼ go2rtc ← 始终拉 rtmp://10.0.10.11:1935/local/\u0026lt;uid\u0026gt; 占位服务使用 mediamtx（bluenviron/mediamtx:latest-ffmpeg），内置 ffmpeg 生成黑屏 H264 帧：\ndocker-compose.yml：\n1 2 3 4 5 6 7 8 services: nanit-placeholder: image: bluenviron/mediamtx:latest-ffmpeg container_name: nanit-placeholder restart: \u0026#34;no\u0026#34; network_mode: host volumes: - ./mediamtx.yml:/mediamtx.yml mediamtx.yml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 rtsp: no rtmp: yes rtmpAddress: :1935 paths: \u0026#34;local/ec06240f\u0026#34;: runOnInit: ffmpeg -re -f lavfi -i color=c=black:s=640x480:r=1 -c:v libx264 -preset ultrafast -profile:v baseline -level 3.0 -g 1 -pix_fmt yuv420p -f flv rtmp://localhost:1935/local/ec06240f runOnInitRestart: yes \u0026#34;local/333ea643\u0026#34;: runOnInit: ffmpeg -re -f lavfi -i color=c=black:s=640x480:r=1 -c:v libx264 -preset ultrafast -profile:v baseline -level 3.0 -g 1 -pix_fmt yuv420p -f flv rtmp://localhost:1935/local/333ea643 runOnInitRestart: yes 几个关键 ffmpeg 参数：\n参数 原因 -profile:v baseline -level 3.0 最大 H264 兼容性，防止 vaapi 解码报错 -g 1 每帧都是关键帧，确保流启动干净，RTSP 中继不出现\u0026quot;Invalid data\u0026quot; 不用 -tune stillimage 该 tune 产生非标准 H264 结构，约 40 秒后 RTSP 中继断流 yang@debian.lan 的 cron：\n1 2 3 4 5 0 9 * * * docker compose -f /home/yang/docker/nanit/docker-compose.yml stop \\ \u0026amp;\u0026amp; docker compose -f /home/yang/docker/nanit-placeholder/docker-compose.yml up -d 0 19 * * * docker compose -f /home/yang/docker/nanit-placeholder/docker-compose.yml down \\ \u0026amp;\u0026amp; docker compose -f /home/yang/docker/nanit/docker-compose.yml up -d 切换过程中有约 2–3 秒断流，go2rtc 重连期间 Frigate 会立刻恢复——这只是短暂的连接中断，而非持续的 404，不会触发 crash loop。\n结果 # 修复后，所有摄像头崩溃次数降为零。此前每天上午因 vaapi 级联故障而耗费数小时恢复的非 nanit 摄像头，现在全天稳定运行。\ngo2rtc 配置保持简洁——每路流只配一个源，无需备用：\n1 2 3 4 5 6 go2rtc: streams: nanit_adelia: - rtmp://10.0.10.11:1935/local/ec06240f nanit_leonard: - rtmp://10.0.10.11:1935/local/333ea643 总结 # 健康检查让这个问题得以曝光。没有每日摘要里的各摄像头崩溃计数，这一切会悄无声息地持续——Frigate 显示绿色，而摄像头的 ffmpeg 进程每天早上循环崩溃数千次，其他摄像头则花半天时间慢慢恢复。\n找到根因之后，修复本身并不复杂。难的是走到那一步。\n","date":"2026年4月3日","externalUrl":null,"permalink":"/zh/posts/frigate-nanit-crash-fix/","section":"文章","summary":"每日 LLM 健康检查报告显示 10 台摄像头中有 8 台当天崩溃次数达到数百次。 追查下去，根因是两台婴儿监控摄像头、一个 go2rtc 重连窗口，以及一次 vaapi 级联崩溃——没有一个环节是直接显而易见的。以下是完整的排查与修复过程。\n问题是如何发现的 # 我在搭建一个每日家庭健康代理——一个定时脚本，查询所有家庭服务（Frigate、Home Assistant、Paperless、arr 媒体栈），然后将数据交给本地 LLM 分析。核心思路是：不再手动逐个检查仪表盘，而是每天早上收到一份摘要，自动标出异常项。\nFrigate 的检查项查询 /api/stats，提取每台摄像头的崩溃次数。某天早上，报告返回了这样的数据：\n1 2 3 4 5 6 nanit_adelia: 2228 次崩溃 nanit_leonard: 2228 次崩溃 backyard: 847 次崩溃 front_door: 391 次崩溃 side_a: 203 次崩溃 ... 如果没有健康检查，我根本不会注意到——Frigate 容器本身从未重启，Web UI 上各摄像头仍然显示\"在线\"，也没有任何告警弹出。\n根因：崩溃级联 # 顺着日志往前追，每天上午 9 点的事件链如下：\n","title":"摄像头崩溃级联排查：LLM 每日健康检查如何发现一个隐藏的 Frigate Bug","type":"posts"},{"content":"在 WSL 中运行 Claude Code 时，很容易错过它等待输入的时刻——尤其是当你切换到其他窗口的时候。本文记录了我搭建的通知系统：当 Claude 停止响应或请求权限时，会弹出 Windows 气泡通知，点击通知可直接聚焦到对应的 WezTerm 面板。\n工作原理 # Claude Code 在 ~/.claude/settings.json 中提供了钩子（hooks）系统，其中两个事件特别有用：\nStop — Claude 完成一次响应、等待用户输入时触发。钩子载荷包含 last_assistant_message、cwd 和 transcript_path。 PermissionRequest — Claude 需要批准才能运行某个工具（Bash 命令、文件写入等）时触发。载荷包含 tool_name 和 tool_input。 两个钩子均以异步方式（async: true）运行 shell 命令，不会阻塞 Claude。\n1 2 3 4 5 6 { \u0026#34;hooks\u0026#34;: { \u0026#34;PermissionRequest\u0026#34;: [{ \u0026#34;hooks\u0026#34;: [{ \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;bash ~/.claude/notify-permission.sh\u0026#34;, \u0026#34;async\u0026#34;: true }] }], \u0026#34;Stop\u0026#34;: [{ \u0026#34;hooks\u0026#34;: [{ \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;bash ~/.claude/notify-stop.sh\u0026#34;, \u0026#34;async\u0026#34;: true }] }] } } 脚本说明 # ~/.claude/notify-stop.sh # 从钩子的标准输入 JSON 中提取标题、消息正文和 cwd，然后启动 PowerShell 发送通知：\n1 2 3 4 5 6 7 8 9 10 11 12 #!/bin/bash INPUT=$(cat) TITLE=$(echo \u0026#34;$INPUT\u0026#34; | jq -r \u0026#39;\u0026#34;Claude [\u0026#34; + (.cwd | split(\u0026#34;/\u0026#34;) | last) + \u0026#34;]\u0026#34;\u0026#39;) BODY=$(echo \u0026#34;$INPUT\u0026#34; | jq -r \u0026#39;.last_assistant_message // \u0026#34;\u0026#34; | .[0:200]\u0026#39;) CWD=$(echo \u0026#34;$INPUT\u0026#34; | jq -r \u0026#39;.cwd\u0026#39;) jq -n --arg t \u0026#34;$TITLE\u0026#34; --arg b \u0026#34;$BODY\u0026#34; --arg c \u0026#34;$CWD\u0026#34; \\ \u0026#39;{title:$t,body:$b,cwd:$c}\u0026#39; \u0026gt; /tmp/claude-notif.json JSON_WIN=$(wslpath -w /tmp/claude-notif.json) PS1_WIN=$(wslpath -w ~/.claude/notify-stop.ps1) powershell.exe -ExecutionPolicy Bypass -File \u0026#34;$PS1_WIN\u0026#34; \u0026#34;$JSON_WIN\u0026#34; ~/.claude/notify-permission.sh # 结构相同，但正文由工具名称和关键参数拼接而成：\n1 2 3 4 5 6 7 BODY=$(echo \u0026#34;$INPUT\u0026#34; | jq -r \u0026#39; (.tool_name) + \u0026#34;: \u0026#34; + ( if .tool_name == \u0026#34;Bash\u0026#34; then .tool_input.command elif (.tool_name == \u0026#34;Write\u0026#34; or .tool_name == \u0026#34;Edit\u0026#34;) then .tool_input.file_path else (.tool_input | tostring) end ) | .[0:200]\u0026#39;) 通知示例：Bash: git status 或 Edit: src/main.go。\n~/.claude/notify-stop.ps1 # PowerShell 脚本完成四件事：\n已聚焦则跳过 — 检查目标 WezTerm 窗口是否已是前台窗口，若是则静默退出。 显示气泡通知 — 在 Windows 系统托盘显示标题和正文。 点击时查找正确面板 — 调用 wezterm.exe cli list --format json，按 cwd 过滤，获取面板 ID 和窗口标题。 激活并提升窗口 — activate-pane 切换到正确标签页，再通过 Win32 API 将窗口置于前台。 难点解析 # 环境变量无法跨越 WSL→PowerShell 边界 # 无法在 bash 中设置变量后用 $Env:VAR 在 PowerShell 中读取。解决方案：用 jq -n 从 bash 写入 JSON 文件，再在 PowerShell 中用 ConvertFrom-Json 读取。\n找到正确的 WezTerm 窗口 # wezterm cli list --format json 以 file:// URI 形式返回每个面板的 cwd（例如 file://pc/home/huyang/workdir）。使用 EndsWith 与钩子中的 cwd（/home/huyang/workdir）匹配：\n1 Where-Object { $_.cwd -and $_.cwd.EndsWith($script:cwd) } 此查询在点击时执行，而非通知弹出时，以确保获取最新的窗口标题。\n窗口标题编码不匹配 # Claude 工作期间，WezTerm 会在活动面板标题前加上 ⠂（盲文点，U+2802）。通过 PowerShell 的 ConvertFrom-Json 读取时，这个字符变成了 Γ£│（UTF-8 字节被误读为 cp1252）；而 Win32 的 GetWindowText 返回的是 ?。\n解决方案：比较前先裁掉两端字符串的非 ASCII、非字母数字前缀：\n1 2 3 4 5 public static string StripPrefix(string s) { int i = 0; while (i \u0026lt; s.Length \u0026amp;\u0026amp; (s[i] \u0026gt; 127 || !char.IsLetterOrDigit(s[i]))) i++; return s.Substring(i); } SetForegroundWindow 被 Windows 拦截 # Windows 会阻止后台进程抢占前台窗口。keybd_event(Alt) 技巧在某些情况下有效，但更可靠的方案是 AttachThreadInput——在调用 SetForegroundWindow 前，先将当前线程附加到前台窗口的线程：\n1 2 3 4 5 6 7 var fg = GetForegroundWindow(); uint fgThread; GetWindowThreadProcessId(fg, out fgThread); uint myThread = GetCurrentThreadId(); AttachThreadInput(myThread, fgThread, true); BringWindowToTop(found); SetForegroundWindow(found); AttachThreadInput(myThread, fgThread, false); 最终效果 # Claude 完成响应 → 弹出 Windows 通知，显示最后一条消息及所属项目 Claude 请求权限 → 通知显示具体命令或文件路径 点击任意通知 → 聚焦到正确的 WezTerm 窗口和面板 若目标窗口已在前台，则不显示通知 ","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/posts/claude-code-wsl-notifications/","section":"文章","summary":"在 WSL 中运行 Claude Code 时，很容易错过它等待输入的时刻——尤其是当你切换到其他窗口的时候。本文记录了我搭建的通知系统：当 Claude 停止响应或请求权限时，会弹出 Windows 气泡通知，点击通知可直接聚焦到对应的 WezTerm 面板。\n工作原理 # Claude Code 在 ~/.claude/settings.json 中提供了钩子（hooks）系统，其中两个事件特别有用：\nStop — Claude 完成一次响应、等待用户输入时触发。钩子载荷包含 last_assistant_message、cwd 和 transcript_path。 PermissionRequest — Claude 需要批准才能运行某个工具（Bash 命令、文件写入等）时触发。载荷包含 tool_name 和 tool_input。 两个钩子均以异步方式（async: true）运行 shell 命令，不会阻塞 Claude。\n1 2 3 4 5 6 { \"hooks\": { \"PermissionRequest\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"bash ~/.claude/notify-permission.sh\", \"async\": true }] }], \"Stop\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"bash ~/.claude/notify-stop.sh\", \"async\": true }] }] } } 脚本说明 # ~/.claude/notify-stop.sh # 从钩子的标准输入 JSON 中提取标题、消息正文和 cwd，然后启动 PowerShell 发送通知：\n","title":"Claude Code WSL 通知与 WezTerm 窗口聚焦","type":"posts"},{"content":"","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/tags/claude-code/","section":"Tags","summary":"","title":"Claude-Code","type":"tags"},{"content":"","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/categories/dev-tools/","section":"Categories","summary":"","title":"Dev Tools","type":"categories"},{"content":"","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/tags/hooks/","section":"Tags","summary":"","title":"Hooks","type":"tags"},{"content":"","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/tags/powershell/","section":"Tags","summary":"","title":"Powershell","type":"tags"},{"content":"","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/tags/wezterm/","section":"Tags","summary":"","title":"Wezterm","type":"tags"},{"content":"","date":"2026年3月24日","externalUrl":null,"permalink":"/zh/tags/wsl/","section":"Tags","summary":"","title":"Wsl","type":"tags"},{"content":"将 Frigate 默认的 SSD MobileNet 检测器替换为 YOLOv9t（tiny），通过 OpenVINO 运行在 Intel N97 的核显上。涵盖模型导出、正确的 Frigate 配置，以及一个会导致 100% 误报的关键坑。\n环境 # 服务器： Intel N97（Debian 13），8 路摄像头 Frigate： 0.17，Docker 运行 检测器： OpenVINO GPU（/dev/dri/renderD128） 原模型： SSD MobileNet v2（内置，300×300） 新模型： YOLOv9t ONNX（320×320，8.3 MB） 为什么换 YOLOv9t？ # Frigate OpenVINO 镜像自带的 SSD MobileNet v2 速度快、资源占用低，但对画面边缘目标和部分遮挡目标的识别能力较弱。YOLOv9t（tiny）在精度上有明显提升，计算量却相差不大——在 320×320 输入、N97 核显约 18ms 推理速度下，跑 8 路摄像头完全够用。\n导出模型 # YOLOv9t 不在 Frigate 内置镜像里，需要自行导出。在宿主机上运行：\n1 2 3 4 5 mkdir -p /home/yang/docker/frigate/config/model_cache docker run --rm \\ -v /home/yang/docker/frigate/config/model_cache:/output \\ ultralytics/ultralytics:latest \\ bash -c \u0026#39;cd /output \u0026amp;\u0026amp; yolo export model=yolov9t.pt format=onnx imgsz=320\u0026#39; 这个命令会下载预训练的 YOLOv9t 权重，以 320×320 输入尺寸导出为 ONNX 格式，并将 yolov9t.onnx（8.3 MB）保存到 Frigate 的 config 目录——该目录在容器内挂载为 /config。\nFrigate 配置 # 模型通过 /config 挂载，容器内路径为 /config/model_cache/yolov9t.onnx。YOLO 模型使用的 80 类 COCO labelmap 已内置于 Frigate 容器，路径为 /labelmap/coco-80.txt。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 detectors: ov: type: openvino device: GPU model: path: /config/model_cache/yolov9t.onnx model_type: yolo-generic input_tensor: nchw input_pixel_format: bgr input_dtype: float width: 320 height: 320 labelmap_path: /labelmap/coco-80.txt 关键坑：不要用 model_type: yolov9 # 不要写 model_type: yolov9。 看起来是对的，但在 Frigate 0.17 中这是无效值。有效值只有：dfine、rfdetr、ssd、yolox、yolonas、yolo-generic。\n写 yolov9 会触发配置校验错误，Frigate 进入 safe mode，悄悄回退到基于 CPU 的 TFLite 备用模型。那个模型对什么都给出接近 100% 的置信度——树叶被检测为人，空车道里全是车。看起来新模型完全失控，但实际上新模型根本没有加载。\n所有 Ultralytics YOLO 模型（v8、v9 等）正确的写法都是 yolo-generic。\n输出张量格式 # 导出的 ONNX 模型输出形状为 [1, 84, 2100]：\n84 个通道 = 4 个边框坐标（xywh，像素坐标）+ 80 个类别分数 2100 = 320×320 输入下的 anchor 点数量 Frigate 的 yolo-generic 处理器会正确地转置并后处理这个输出，包括坐标归一化和 NMS，无需额外配置。\n推理速度 # 切换后，OpenVINO 检测器推理速度约 18ms，对于 8 路 1–5 FPS 的摄像头流完全够用。\n1 2 Detector: ov inference_speed: 18.5 ms API 分数显示 Bug # 在 Frigate 0.17 中，events API 顶层的 top_score 字段对自定义模型检测到的事件返回 null。真实分数嵌套在 data.score 和 data.top_score 里。这是一个显示 bug——检测本身正常工作，完整事件 JSON 中可以看到真实置信度（比如停放车辆 0.75–0.88）。\n阈值 # 原来各摄像头的 min_score 和 threshold 是针对 SSD 的置信度分布单独调过的。YOLOv9t 整体精度更高，重置为 Frigate 默认值（min_score: 0.5，threshold: 0.7），只保留各摄像头的遮罩区域（mask）。后续根据实际检测情况再微调。\n","date":"2026年3月19日","externalUrl":null,"permalink":"/zh/posts/frigate-yolov9t-openvino/","section":"文章","summary":"将 Frigate 默认的 SSD MobileNet 检测器替换为 YOLOv9t（tiny），通过 OpenVINO 运行在 Intel N97 的核显上。涵盖模型导出、正确的 Frigate 配置，以及一个会导致 100% 误报的关键坑。\n环境 # 服务器： Intel N97（Debian 13），8 路摄像头 Frigate： 0.17，Docker 运行 检测器： OpenVINO GPU（/dev/dri/renderD128） 原模型： SSD MobileNet v2（内置，300×300） 新模型： YOLOv9t ONNX（320×320，8.3 MB） 为什么换 YOLOv9t？ # Frigate OpenVINO 镜像自带的 SSD MobileNet v2 速度快、资源占用低，但对画面边缘目标和部分遮挡目标的识别能力较弱。YOLOv9t（tiny）在精度上有明显提升，计算量却相差不大——在 320×320 输入、N97 核显约 18ms 推理速度下，跑 8 路摄像头完全够用。\n","title":"Frigate 换用 YOLOv9t + OpenVINO（Intel N97）","type":"posts"},{"content":"","date":"2026年3月19日","externalUrl":null,"permalink":"/zh/tags/openvino/","section":"Tags","summary":"","title":"Openvino","type":"tags"},{"content":"","date":"2026年3月19日","externalUrl":null,"permalink":"/zh/tags/surveillance/","section":"Tags","summary":"","title":"Surveillance","type":"tags"},{"content":"","date":"2026年3月19日","externalUrl":null,"permalink":"/zh/tags/yolo/","section":"Tags","summary":"","title":"Yolo","type":"tags"},{"content":"","date":"2026年3月18日","externalUrl":null,"permalink":"/zh/tags/debian/","section":"Tags","summary":"","title":"Debian","type":"tags"},{"content":"在 Debian 服务器（Intel N97）上部署 Frigate NVR，替代传统 NVR 的检测功能。 涵盖 Docker Compose、go2rtc 流配置、硬件加速、HA 集成、推送通知及基于区域的告警。 VLAN 间流量经由主路由器（UCG Ultra）转发。\n硬件与背景 # 服务器： Intel N97 迷你主机（debian.lan，10.0.10.11），Debian 13 摄像头： 8 台 Reolink PoE 摄像头（摄像头 VLAN，10.0.40.0/24），2 台 Nanit 婴儿监控器（IoT VLAN，10.0.20.0/24） 现有 NVR： 保留运行，负责持续录像；Frigate 专注于检测和事件片段 Home Assistant： 位于 IoT VLAN（10.0.20.10），已运行 MQTT Broker 存储设计 # 录像存储在 NAS 专用共享目录，避免占用本地 NVMe。\n在 Synology DSM 中：创建共享文件夹 surveillance，NFS 导出给 10.0.10.11， 权限选择 Map all users to admin（将所有 UID 映射为 admin）， 避免 Frigate 容器 UID（1001）在 NAS 上找不到对应用户的问题。\n在 debian 上挂载：\n1 2 sudo mkdir -p /mnt/nas-surveillance sudo mount -t nfs 10.0.10.10:/volume1/surveillance /mnt/nas-surveillance 添加到 /etc/fstab：\n1 10.0.10.10:/volume1/surveillance /mnt/nas-surveillance nfs vers=3,rw,_netdev,nofail,soft,rsize=65536,wsize=65536,timeo=300 0 0 Docker Compose # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # /home/yang/docker/frigate/docker-compose.yml services: frigate: container_name: frigate image: ghcr.io/blakeblackshear/frigate:stable restart: unless-stopped shm_size: \u0026#34;256mb\u0026#34; devices: - /dev/dri/renderD128:/dev/dri/renderD128 volumes: - /etc/localtime:/etc/localtime:ro - ./config:/config - /mnt/nas-surveillance/frigate:/media/frigate ports: - \u0026#34;5000:5000\u0026#34; - \u0026#34;8554:8554\u0026#34; - \u0026#34;8555:8555/tcp\u0026#34; - \u0026#34;8555:8555/udp\u0026#34; env_file: docker-compose.env 1 2 3 4 5 # docker-compose.env FRIGATE_RTSP_PASSWORD=... FRIGATE_DOORBELL_PASSWORD=... # 门铃密码不同，单独设置 FRIGATE_MQTT_USER=... FRIGATE_MQTT_PASSWORD=... 密码通过环境变量注入，在 config.yml 中以 {FRIGATE_VARIABLE_NAME} 引用， 避免明文写入配置文件。\nshm_size: 256mb — 帧缓冲的共享内存。8 路摄像头 5fps 检测流约需 20–30 MB， 256 MB 留有充足余量。\n/dev/dri/renderD128 — Intel 核显，用于 VAAPI 硬件解码。 无需 privileged: true，直接传递设备即可。\nconfig.yml # MQTT 与硬件加速 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mqtt: enabled: true host: 10.0.20.10 # HA 的 IP（IoT VLAN） port: 1883 user: \u0026#34;{FRIGATE_MQTT_USER}\u0026#34; password: \u0026#34;{FRIGATE_MQTT_PASSWORD}\u0026#34; ffmpeg: hwaccel_args: preset-vaapi detectors: ov: type: openvino device: GPU model: path: /openvino-model/ssdlite_mobilenet_v2.xml labelmap_path: /openvino-model/coco_91cl_bkgr.txt width: 300 height: 300 ffmpeg 与检测器加速的区别： preset-vaapi 负责视频解码（H.264/H.265 → 原始帧），使用 Intel 核显的媒体引擎。OpenVINO 检测器负责推理（运行目标检测模型），使用核显的 EU 计算单元。两者共用同一块 N97 核显，但使用不同的硬件模块，互不干扰。\n为什么选 OpenVINO 而非 CPU？ N97 原生支持 OpenVINO，GPU 推理比 CPU 快 3–5 倍，无需额外硬件。模型（ssdlite_mobilenet_v2）完全相同，只是换了执行后端。\nmodel: 必须放在顶层。 这是全局配置块，不能嵌套在 detectors 下面。如果把 path 写在 detectors.ov.model 内，Frigate 会读到 None 路径并在启动时崩溃。\nMQTT host 填 HA 的 IP（10.0.20.10），流量经 UCG Ultra 路由转发。\n录像策略 # 1 2 3 4 5 6 7 8 record: enabled: true alerts: retain: days: 7 detections: retain: days: 7 顶层没有 retain.days，意味着不做持续录像，只保存事件片段。 现有 NVR 负责 24/7 全程录像，Frigate 保存带标签的短片段，两者互补。\ngo2rtc 流配置——常见错误 # Frigate 内置 go2rtc 管理视频流。每台摄像头需要两个命名流： 主流（高分辨率，用于录像）和子流（低分辨率，用于检测）。 关键错误：\n错误写法 — 主流和子流写在同一个流名下：\n1 2 3 4 5 go2rtc: streams: front: - rtsp://admin:pass@10.0.40.124:554/h264Preview_01_main - rtsp://admin:pass@10.0.40.124:554/h264Preview_01_sub go2rtc 将多个源视为备用关系，只选其中一个。 后续引用 front_sub 会返回 404。\n正确写法 — 分别命名：\n1 2 3 4 5 6 go2rtc: streams: front: - \u0026#34;rtsp://admin:{FRIGATE_RTSP_PASSWORD}@10.0.40.124:554/h264Preview_01_main\u0026#34; front_sub: - \u0026#34;rtsp://admin:{FRIGATE_RTSP_PASSWORD}@10.0.40.124:554/h264Preview_01_sub\u0026#34; 摄像头配置中分别引用：\n1 2 3 4 5 6 7 8 cameras: front: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_sub roles: [detect] - path: rtsp://127.0.0.1:8554/front roles: [record] Reolink RTSP 地址格式 # 标准 Reolink 摄像头：\n主流：rtsp://admin:pass@IP:554/h264Preview_01_main 子流：rtsp://admin:pass@IP:554/h264Preview_01_sub 较新型号（如 CX810）使用 H.265：\nrtsp://admin:pass@IP:554/h265Preview_01_main Reolink Duo 有两个镜头，路径为 Preview_01 和 Preview_02。 实际使用中只有一个镜头有效，当作单摄像头处理即可。\n摄像头配置 # 1 2 3 4 5 6 7 8 9 10 11 12 13 cameras: front: ffmpeg: inputs: - path: rtsp://127.0.0.1:8554/front_sub roles: [detect] - path: rtsp://127.0.0.1:8554/front roles: [record] detect: enabled: true # 必须显式设置——0.17 默认为 false width: 640 height: 360 # 与子流实际分辨率匹配 fps: 5 detect.enabled: true 必须显式写明。 Frigate 0.17 中默认为 false。 启动时如果视频流连接失败，Frigate 会自动禁用检测，且流恢复后不会自动重启。 务必在每台摄像头配置中显式设置。\nwidth/height 必须与子流实际分辨率匹配。 Frigate 在送入检测模型前会将帧缩放到该分辨率。如果不匹配，画面会被拉伸，导致目标形状变形，检测准确度下降——对远距离或小目标尤其明显。用 ffprobe 探测子流实际分辨率：\n1 2 3 4 docker exec frigate /usr/lib/ffmpeg/7.0/bin/ffprobe \\ -v error -select_streams v:0 \\ -show_entries stream=width,height -of csv=p=0 \\ rtsp://127.0.0.1:8554/front_sub 我的摄像头子流分辨率：\n摄像头 子流分辨率 备注 Reolink 标准型（front、backyard、side_a/b、cx810） 640×360 16:9 Reolink E1 640×360 PTZ，不设 zone Reolink 门铃 480×640 竖屏——宽高互换 Reolink Duo（duo_a） 1536×576 超宽 8:3；检测配置为 1280×480 检测参数调优 # 默认阈值：min_score: 0.5，threshold: 0.7。 threshold 是跟踪窗口内的置信度滑动平均值——间歇性低置信度检测可能达不到该阈值。\n对于广角或远距摄像头，降低阈值、提高检测分辨率有明显效果：\n1 2 3 4 5 6 7 8 9 10 11 duo_a: detect: enabled: true width: 1280 # 默认 640——更多像素，远距目标识别更准 height: 720 fps: 5 objects: filters: person: min_score: 0.45 threshold: 0.55 区域与告警 # Frigate 0.17 将事件分为 alerts（告警） 和 detections（检测） 两类。 默认情况下，画面任何位置的 person 和 car 均触发告警。区域配置可限制这一行为。\n工作原理 # 区域多边形在 Frigate UI 中绘制（可视化编辑器），自动写入 config.yml 区域的 objects 列表限定哪些类别可激活该区域 loitering_time 要求目标在区域内停留 N 秒后才触发 required_zones 写在 review.alerts 下，控制哪些事件升级为告警 required_zones 的正确位置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cameras: duo_a: review: alerts: required_zones: - frontyard-parking-zone - person-driveway-loister-zone zones: frontyard-parking-zone: coordinates: \u0026#34;...\u0026#34; loitering_time: 180 # 3 分钟 objects: - car person-driveway-loister-zone: coordinates: \u0026#34;...\u0026#34; loitering_time: 30 # 30 秒 objects: - person required_zones 放在 cameras.\u0026lt;name\u0026gt;.review.alerts 下—— 不是 objects.filters 下（该位置不存在此键，会导致配置报错）。\n效果 # 事件 结果 车辆路过 仅检测，不告警 车辆在区域内停留 3 分钟以上 触发告警 行人路过 仅检测，不告警 行人在区域内停留 30 秒以上 触发告警 Home Assistant 集成 # Frigate 集成 # 在 HA 中：设置 → 集成 → 添加 → Frigate\n填写 Frigate 的主 LAN 地址：http://10.0.10.11:5000\n推送通知 # 通过 HA 自动化实现带截图的推送通知：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 alias: Frigate person notification triggers: - trigger: mqtt topic: frigate/events conditions: - condition: template value_template: \u0026gt; {{ trigger.payload_json.type == \u0026#39;new\u0026#39; and trigger.payload_json.after.label == \u0026#39;person\u0026#39; and trigger.payload_json.after.camera in [\u0026#39;doorbell\u0026#39;, \u0026#39;e1\u0026#39;] }} actions: - delay: \u0026#34;00:00:02\u0026#34; - action: notify.mobile_app_yangs_iphone data: title: \u0026#34;{{ trigger.payload_json.after.camera | title }}: 检测到人\u0026#34; message: \u0026#34;{{ now().strftime(\u0026#39;%H:%M\u0026#39;) }}\u0026#34; data: image: \u0026#34;http://10.0.10.11:5000/api/events/{{ trigger.payload_json.after.id }}/snapshot.jpg\u0026#34; url: \u0026#34;http://frigate.tailnet:5000/review?camera={{ trigger.payload_json.after.camera }}\u0026#34; push: sound: name: default 为什么用 type == 'new' 而不是 type == 'end'？ end 触发时可以提供完整视频片段，但通知会延迟—— 且如果人一直停留在画面中，end 永远不会触发。 new 立即触发；延迟 2 秒等待 Frigate 生成首帧截图。\n截图 URL 使用 Frigate 的 LAN 地址（10.0.10.11），流量经主路由转发。 点击跳转 URL 使用 Tailscale 域名，支持远程访问。\n踩坑总结 # go2rtc 流名称必须按分辨率分别命名。 主流和子流写在同一个名称下， 子流通过 go2rtc RTSP 服务器访问时返回 404。\nFrigate 0.17 中 detect.enabled 默认为 false。 必须在每台摄像头配置 中显式声明。启动时流连接失败会导致检测被自动禁用，且不会自动恢复。\nrequired_zones 放在 review.alerts 下，不是 objects.filters。 放错位置会导致配置校验报错。\n部分摄像头默认关闭 RTSP。 较新的 Reolink 型号（如 CX810）需要在 摄像头 Web 界面手动开启 RTSP，端口 554 connection refused 即为此症状。\nH.265 摄像头使用不同的 RTSP 路径。 需将 h264Preview_01_main 改为 h265Preview_01_main。\n实时画面和检测是相互独立的。 浏览器通过 WebRTC 直接从 go2rtc 获取视频， 即使检测功能异常，实时画面仍然正常显示。能看到直播流不代表检测在运行。\nHA↔Frigate 流量经主路由转发。 需确保 UCG Ultra 防火墙允许 IoT VLAN → 主 LAN 的 5000 和 1935 端口通行。\n","date":"2026年3月18日","externalUrl":null,"permalink":"/zh/posts/frigate-setup/","section":"文章","summary":"在 Debian 服务器（Intel N97）上部署 Frigate NVR，替代传统 NVR 的检测功能。 涵盖 Docker Compose、go2rtc 流配置、硬件加速、HA 集成、推送通知及基于区域的告警。 VLAN 间流量经由主路由器（UCG Ultra）转发。\n硬件与背景 # 服务器： Intel N97 迷你主机（debian.lan，10.0.10.11），Debian 13 摄像头： 8 台 Reolink PoE 摄像头（摄像头 VLAN，10.0.40.0/24），2 台 Nanit 婴儿监控器（IoT VLAN，10.0.20.0/24） 现有 NVR： 保留运行，负责持续录像；Frigate 专注于检测和事件片段 Home Assistant： 位于 IoT VLAN（10.0.20.10），已运行 MQTT Broker 存储设计 # 录像存储在 NAS 专用共享目录，避免占用本地 NVMe。\n","title":"Frigate NVR 配置全记录：从 Docker 到 HA 推送通知","type":"posts"},{"content":"","date":"2026年3月18日","externalUrl":null,"permalink":"/zh/tags/home-assistant/","section":"Tags","summary":"","title":"Home-Assistant","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/ai/","section":"Tags","summary":"","title":"Ai","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/llm/","section":"Tags","summary":"","title":"Llm","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/media/","section":"Tags","summary":"","title":"Media","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/nas/","section":"Tags","summary":"","title":"Nas","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/ollama/","section":"Tags","summary":"","title":"Ollama","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/paperless/","section":"Tags","summary":"","title":"Paperless","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/plex/","section":"Tags","summary":"","title":"Plex","type":"tags"},{"content":"","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/tags/synology/","section":"Tags","summary":"","title":"Synology","type":"tags"},{"content":"本文是一份完整的操作手册，介绍如何通过 paperless-ai 和本地运行的 Ollama 为 paperless-ngx 集成 AI 自动标签和分类功能。整套方案使用本地大语言模型读取文档文本，自动填充元数据字段——包括标题、文档类型、标签、联系人、日期以及自定义字段。\n硬件与架构 # NAS（群晖 DS1621+，10.0.10.10）：在 5656 端口运行 paperless-ngx 台式 PC：Windows，安装了 WSL2、Docker Desktop，配备 RTX 4090 目标：使用本地 LLM 实现 AI 自动打标/分类，零云端依赖 核心架构决策是拉取模式（pull model）：paperless-ai 运行在 WSL2 的 Docker 容器中，轮询 paperless-ngx API 寻找带有 ai-pending 标签的文档，调用 Ollama 处理后将元数据写回。对于不是 24 小时开机的台式机而言，这是最正确的方案——NAS 保存待处理队列，台式机开机后自动消费。\n1 2 3 4 5 6 7 paperless-ngx (NAS) ↑ ↓ (REST API) paperless-ai (WSL2 Docker) ↑ ↓ (HTTP) Ollama (Windows 原生) ↑ RTX 4090 (GPU) Ollama 以原生方式运行在 Windows 上（而非 WSL 内），以获得最佳 GPU 访问性能。在 WSL2 的 Docker 容器内，通过特殊主机名 host.docker.internal 访问 Ollama。\n前置条件 # paperless-ngx 已运行并可通过 API 访问 Windows 上已安装 Docker Desktop，并启用了 WSL2 集成 Windows 上已安装 Ollama 第一步 — 让 Ollama 监听所有网络接口 # Ollama 默认只监听 127.0.0.1，导致 WSL2 Docker 容器无法访问。需要设置一个 Windows 系统环境变量。\n打开系统属性 → 高级 → 环境变量 在系统变量下点击新建 变量名：OLLAMA_HOST 变量值：0.0.0.0 点击确定，然后重启 Ollama（关闭系统托盘图标后重新启动） 在 WSL2 中验证：\n1 curl http://$(ip route | awk \u0026#39;/default/ {print $3}\u0026#39;):11434/api/tags 从 Docker 容器内部，Ollama 可通过 host.docker.internal:11434 访问。\n第二步 — 拉取正确的模型 # 所用模型必须支持 Ollama 结构化输出（即 format / JSON Schema 参数）。该功能通过约束 token 级别的解码来强制输出 JSON，并非所有模型都支持。\n关键注意：qwen3-vl:8b（视觉-语言多模态变体）不支持结构化输出。传入 format schema 时，Ollama 会静默返回空响应字符串。这个失败是无声的，非常难以排查。\n请使用 qwen3:8b（基础模型）：\n1 2 # 在 Windows PowerShell 中运行 ollama pull qwen3:8b 测试结构化输出是否正常工作：\n1 2 3 4 5 6 curl http://localhost:11434/api/generate -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;qwen3:8b\u0026#34;, \u0026#34;format\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {\u0026#34;title\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;}}, \u0026#34;required\u0026#34;: [\u0026#34;title\u0026#34;]}, \u0026#34;prompt\u0026#34;: \u0026#34;Return a JSON object with a title field set to hello world.\u0026#34;, \u0026#34;stream\u0026#34;: false }\u0026#39; 响应中的 response 字段应为非空的 JSON 字符串。如果是 \u0026quot;\u0026quot;，说明该模型不支持结构化输出。\n第三步 — 在 paperless-ngx 中创建标签 # 在 paperless-ngx 中创建两个标签（设置 → 标签）：\n标签 用途 ai-pending 输入过滤器——拥有此标签的文档将被 paperless-ai 处理 ai-processed 输出标记——paperless-ai 处理成功后添加此标签 两个标签的匹配算法均设为无（它们由工作流和 paperless-ai 分配，不使用自动匹配规则）。\n通过 API 可验证标签 ID（无需用到，但便于调试）：\n1 2 curl -s http://10.0.10.10:5656/api/tags/ \\ -H \u0026#34;Authorization: Token \u0026lt;YOUR_TOKEN\u0026gt;\u0026#34; | python3 -m json.tool | grep -A3 \u0026#34;ai-pending\u0026#34; 第四步 — 在 paperless-ngx 中创建工作流 # paperless-ai 从不移除标签——它只会添加标签。ai-pending 标签必须在处理完成后通过工作流移除。在 paperless-ngx 中设置两个工作流（设置 → 工作流）：\n工作流 1：\u0026ldquo;AI 处理队列\u0026rdquo; # 触发条件：文档已添加 操作：分配标签 ai-pending 确保每一份新添加的文档都自动进入 AI 处理队列。\n工作流 2：\u0026ldquo;AI 处理完成后移除 ai-pending\u0026rdquo; # 触发条件：文档已更新——含有标签 ai-processed 操作：移除标签 ai-pending 在 paperless-ai 完成处理后清理队列标记。如果没有这个工作流，ai-pending 标签会一直留在每份文档上，Ollama 会反复重新处理它们。\n第五步 — 创建 paperless-ai 项目文件 # 创建项目目录：\n1 2 mkdir -p ~/repo/paperless-ai cd ~/repo/paperless-ai docker-compose.yml # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 services: paperless-ai: image: clusterzx/paperless-ai container_name: paperless-ai restart: unless-stopped user: \u0026#34;0:0\u0026#34; env_file: - .env ports: - \u0026#34;3000:3000\u0026#34; volumes: - paperless-ai_data:/app/data volumes: paperless-ai_data: user: \u0026quot;0:0\u0026quot; 指令至关重要。paperless-ai 在 /app/data 目录内写入配置文件和 SQLite 数据库。在 WSL2/Docker Desktop 环境下，权限映射问题会导致默认的 node 用户无法在 volume 中创建文件——以 root 身份运行可彻底规避这些问题。\n.env # 1 2 3 4 5 6 7 8 9 10 11 12 PAPERLESS_API_URL=http://10.0.10.10:5656/api PAPERLESS_API_TOKEN=\u0026lt;YOUR_TOKEN\u0026gt; PAPERLESS_USERNAME=yang AI_PROVIDER=ollama OLLAMA_API_URL=http://host.docker.internal:11434 OLLAMA_MODEL=qwen3:8b SCAN_INTERVAL=*/30 * * * * PROCESS_PREDEFINED_DOCUMENTS=yes TAGS=ai-pending ADD_AI_PROCESSED_TAG=yes AI_PROCESSED_TAG_NAME=ai-processed USE_EXISTING_DATA=yes 关键配置说明：\nTAGS=ai-pending：paperless-ai 只处理带有该标签的文档 SCAN_INTERVAL=*/30 * * * *：每 30 分钟轮询一次 paperless-ngx PROCESS_PREDEFINED_DOCUMENTS=yes：处理已存在的文档（不只是新文档） ADD_AI_PROCESSED_TAG=yes：处理后添加 ai-processed 标签（清理工作流必须依赖此标签） USE_EXISTING_DATA=yes：不用 AI 结果覆盖原始空字段 第六步 — 编写系统提示词 # paperless-ai 将文档文本连同你的自定义系统提示词一起发送给 Ollama。提示词从容器内的 /app/data/PROMPT.md 文件读取（也可通过 http://localhost:3000 的 Web UI 设置）。\n提示词应定义：\n存在哪些文档类型（使用一致的命名） 有哪些主题标签可用 需要填写哪些自定义字段 边界情况的明确规则 针对本套方案的提示词工程经验总结：\n明确列举所有合法值——不要让模型自己发明文档类型或标签 明确禁止保留标签——如果有人工管理的状态标签，将其列为绝对禁止项 要求自定义字段值为字符串类型——paperless-ai 期望所有自定义字段值为字符串；在提示词中注明：\u0026ldquo;所有自定义字段的值必须是字符串（加引号）或 null。写 \u0026quot;2017.08\u0026quot; 而不是 2017.08\u0026rdquo; 对歧义情况给出明确示例——例如：\u0026ldquo;医疗账单 → 使用类型 发票收据 + 标签 #医疗，而非类型 医疗记录\u0026rdquo; 示例提示词结构（部分）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 你是一个个人家庭档案的文档分类助手。 ## 文档类型（精确选择一个） - 发票收据：账单、发票、收据 - 税务文件：W-2、1099、税务申报表 - 移民文件：签证、护照、I-94 ... ## 标签（仅从以下列表中选择所有适用的） - #保险 - #医疗 - #财务 ... ## 自定义字段 - Amount（金额）：金额，字符串格式，例如 \u0026#34;2017.08\u0026#34;，或 null - Bill Period（账单周期）：对账单周期结束日期，YYYY-MM-DD 格式，或 null - Expiry Date（到期日期）：证件/签证/保险到期日，YYYY-MM-DD 格式，或 null - Document Year（文档年份）：年份字符串，例如 \u0026#34;2024\u0026#34;，或 null - Account / Policy Number（账号/保单号）：账号或保单号字符串，或 null ## 规则 - 所有自定义字段值必须是字符串（加引号）或 null - 医疗账单 → 类型：发票收据，标签：#医疗——不要使用类型 医疗记录 - 任何情况下都绝对不得分配：#待处理 #重要 #归档——这些是保留的人工状态标签， 任何文档类型、任何内容都绝不能出现在你的输出中 第七步 — 启动容器 # 1 2 cd ~/repo/paperless-ai docker compose up -d 在 http://localhost:3000 打开 Web UI 验证配置。该界面支持查看和编辑配置，以及手动触发扫描。\n重要：通过 Web UI 保存配置后，权威配置存储在 Docker volume 内部的 /app/data/.env 文件中。docker-compose.env 文件设置初始环境变量；UI 会写入自己的配置文件，某些设置以该文件为准。如果编辑了 .env 并需要容器读取新值，请使用 docker compose up -d（而非 docker compose restart——restart 命令不会重新读取 env 文件）。\n故障排查 # Docker 守护进程未运行 # 1 Error response from daemon: dial unix /var/run/docker.sock: no such file or directory 在 Windows 上启动 Docker Desktop。建议在 Docker Desktop 设置中启用\u0026quot;开机自动启动\u0026quot;。\nOllama 在 WSL2 中不可访问 # 1 connect ETIMEDOUT 10.255.255.254:11434 说明 OLLAMA_HOST=0.0.0.0 未设置，或设置后未重启 Ollama。在 PowerShell 中验证 Ollama 的监听地址：\n1 netstat -ano | findstr 11434 本地地址应显示 0.0.0.0:11434，而非 127.0.0.1:11434。\n.env 修改未生效 # docker compose restart 不会重新读取 env_file。始终使用：\n1 docker compose up -d 这会重新创建容器并加载新的环境变量。\n结构化输出返回空响应 # 1 No response data from Ollama API 所用模型不支持 Ollama 的 format 参数。检查当前运行的模型：\n1 curl http://localhost:11434/api/tags 将所有 *-vl 变体替换为基础模型，例如将 qwen3-vl:8b 替换为 qwen3:8b。\n自定义字段值类型错误 # 1 TypeError: customField.value?.trim is not a function AI 返回了数值类型（如 2017.08），而 paperless-ai 期望字符串（\u0026quot;2017.08\u0026quot;）。在系统提示词中添加规则：\u0026ldquo;所有自定义字段的值必须是字符串（加引号）或 null。\u0026rdquo;\n自定义字段名中的 # 字符导致 env 解析错误 # 1 SyntaxError: Unterminated string in JSON at position 44 名为 Account / Policy # 之类的自定义字段包含 #，该字符在 .env 文件解析时被视为注释符。在 paperless-ngx 中将字段重命名以去掉 #，例如改为 Account / Policy Number。通过 API 重命名：\n1 2 3 4 curl -X PATCH http://10.0.10.10:5656/api/custom_fields/5/ \\ -H \u0026#34;Authorization: Token \u0026lt;YOUR_TOKEN\u0026gt;\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;Account / Policy Number\u0026#34;}\u0026#39; 处理后 ai-pending 标签未被移除 # 文档处理完成后标签仍然存在，说明清理工作流未设置或未触发。检查：\n设置 → 工作流中存在\u0026quot;AI 处理完成后移除 ai-pending\u0026quot;工作流 触发条件为：文档已更新，且含有标签 ai-processed 操作为：移除标签 ai-pending 记住：paperless-ai 源码中会合并标签，从不移除任何标签。移除操作必须依赖工作流。\nsed 命令意外修改无关环境变量 # 如果用 sed 编辑容器内的 /app/data/.env，需注意子字符串匹配问题。例如：\n1 sed -i \u0026#39;s/CUSTOM_FIELDS=.*/NEW_VALUE/\u0026#39; /app/data/.env 这条命令同样会匹配 ACTIVATE_CUSTOM_FIELDS=（因为 CUSTOM_FIELDS 是其子字符串）。改用 Python 加锚定模式：\n1 2 3 4 5 6 python3 -c \u0026#34; import re, sys content = open(\u0026#39;/app/data/.env\u0026#39;).read() content = re.sub(r\u0026#39;^CUSTOM_FIELDS=.*\u0026#39;, \u0026#39;CUSTOM_FIELDS=NEW_VALUE\u0026#39;, content, flags=re.MULTILINE) open(\u0026#39;/app/data/.env\u0026#39;, \u0026#39;w\u0026#39;).write(content) \u0026#34; AI 分配了被禁止的状态标签 # 模型偶尔会分配你为人工保留的标签。在提示词中加强禁止措辞：\n1 2 任何情况下都绝对不得分配：#待处理 #重要 #归档——这些是保留的人工状态标签， 无论文档类型如何、内容是什么，都绝不能出现在你的输出中。 paperless-ai 内部工作原理 # 理解内部机制有助于调试：\npaperless-ai 轮询 paperless-ngx API，寻找带有 ai-pending 标签的文档 对每份文档，获取完整的文本内容 将文本和系统提示词一起以 format: jsonSchema 参数发送给 Ollama Ollama 使用约束解码（在 token 采样层面强制执行），生成合法的 JSON paperless-ai 解析响应：title、document_type、tags、correspondent、document_date、language、custom_fields 调用 paperlessService.updateDocument()，该方法合并标签：[...new Set([...currentDoc.tags, ...updates.tags])]——从不移除标签 添加 ai-processed 标签以表示完成 paperless-ngx 工作流检测到 ai-processed 后，移除 ai-pending 文件权限深度解析 # docker-compose 中的 user: \u0026quot;0:0\u0026quot; 设置值得详细说明。paperless-ai 的基础镜像以 node 用户身份运行。命名 Docker volume 的根目录由 root:root 所有，权限为 755。node 用户可以读取和进入该目录，但无法在其中创建新文件（应用采用原子写入：先创建临时文件，再重命名——两个操作都需要对目录的写入权限）。以 root 身份运行可绕过所有这些问题。\n另一种思路——改用 bind mount——在 WSL2/Docker Desktop 环境下也行不通，因为 WSL2 与 Windows 之间的 uid/gid 映射会导致 SQLite 无法创建数据库文件。\n日常使用 # paperless-ai 在启动时处理文档，之后按 SCAN_INTERVAL 每 30 分钟轮询一次 在 http://localhost:3000 监控状态和手动触发扫描 Web UI 显示处理历史和当前队列状态 在 paperless-ngx 中批量移除标签：列表视图 → 选择一份文档 → 出现\u0026quot;选择全部 X 份文档\u0026quot;→ 操作 → 编辑标签 方案总结 # 组件 位置 备注 paperless-ngx NAS 10.0.10.10:5656 文档存储与 API paperless-ai WSL2 Docker，端口 3000 协调 AI 处理流程 Ollama Windows 原生，端口 11434 GPU 加速 LLM 推理 模型 qwen3:8b 基础模型，非 VL 变体 触发标签 ai-pending 由 paperless-ngx 工作流添加 完成标签 ai-processed 由 paperless-ai 添加 整套流程完全自托管、GPU 加速，无需任何云服务。文档在本地处理，完全保护隐私。\n后续修复 # \u0026ldquo;限制为现有文档类型\u0026quot;设置无效（已知 Bug） # paperless-ai 提供了一个 UI 开关，用于限制只使用已有的文档类型，但截至 2026 年 3 月该功能完全失效（#834、#799）。根本原因：services/paperlessService.js 中的 getOrCreateDocumentType() 没有限制逻辑，而 getOrCreateCorrespondent() 已正确实现。修复 PR #865 已提交但被关闭，未合并。\n**解决方法：**将文件从容器中复制出来，打补丁，再通过 bind mount 挂载回去。\n1 docker cp paperless-ai:/app/services/paperlessService.js ./paperlessService.js 修改 paperlessService.js，参照 getOrCreateCorrespondent 的写法加上限制逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 修改前： async getOrCreateDocumentType(name) { // 修改后： async getOrCreateDocumentType(name, options = {}) { const restrictToExistingDocumentTypes = options.restrictToExistingDocumentTypes === true || (options.restrictToExistingDocumentTypes === undefined \u0026amp;\u0026amp; process.env.RESTRICT_TO_EXISTING_DOCUMENT_TYPES === \u0026#39;yes\u0026#39;); // 在搜索 existingDocType 之后、创建之前加入： if (restrictToExistingDocumentTypes) { console.log(`[DEBUG] Document type \u0026#34;${name}\u0026#34; does not exist and restrictions are enabled, returning null`); return null; } 在 docker-compose.yml 中挂载补丁文件：\n1 2 3 volumes: - paperless-ai_data:/app/data - ./paperlessService.js:/app/services/paperlessService.js:ro 执行 docker compose up -d 重建容器。bind mount 在镜像更新后依然有效。待上游修复后可移除。\nAI 将 correspondent 设置为字符串 \u0026ldquo;null\u0026rdquo; # 模型在找不到机构名时会输出字符串 \u0026quot;null\u0026quot;，导致 paperless-ai 创建一个名为 null 的 correspondent。修复方式：在 prompt 中明确说明该字段应使用 JSON null（而非字符串 \u0026ldquo;null\u0026rdquo;），并将模板中的占位符改为无引号的 null：\n1 2 3 \u0026#34;correspondent\u0026#34;: \u0026#34;Name or null\u0026#34;, ← 提示模型 ... ## Correspondent：不明确时填 JSON null，不是字符串 \u0026#34;null\u0026#34; 通过 API 清理误创建的 null correspondent：\n1 2 3 4 5 # 查找并删除 curl -s http://NAS:5656/api/correspondents/ -H \u0026#34;Authorization: Token TOKEN\u0026#34; | \\ python3 -c \u0026#34;import sys,json; [print(c[\u0026#39;id\u0026#39;], c[\u0026#39;name\u0026#39;]) for c in json.load(sys.stdin)[\u0026#39;results\u0026#39;]]\u0026#34; curl -X DELETE http://NAS:5656/api/correspondents/ID/ -H \u0026#34;Authorization: Token TOKEN\u0026#34; 文档类型分类体系重设计 # 运行一段时间后，发票收据 变成了万能桶——酒店预订确认、维修估价、项目报价、通行证全堆在里面。解决方案是新增两个精准分类，而不是继续强行套用同一个 type。\n新增两个文档类型：\n类型 适用场景 行程预订 酒店/机票确认单、活动门票、通行证、SNO-PARK 等许可证 报价估价 维修估价、施工报价、项目提案——尚未付款的询价类文件 删除了所有 AI 自行创建的 0 文档英文类型（Estimate、Invoice、Quote、repair_estimate、Travel Itinerary、Technical Manual、Product Manual、manual，ids 20–27）。\n重新分类 ~9 个文档：酒店确认单 → 行程预订，航空行程单 → 行程预订，车辆/房屋维修估价 → 报价估价，施工授权书 → 合同协议。\n在 prompt 中新增了防误分类规则：\n1 2 3 - 估价/报价（未付款）→ \u0026#34;报价估价\u0026#34;；已付发票 → \u0026#34;发票收据\u0026#34; - 酒店/机票确认单 → \u0026#34;行程预订\u0026#34;（不是\u0026#34;发票收据\u0026#34;） - 施工授权书、装修合同 → \u0026#34;合同协议\u0026#34;（不是\u0026#34;发票收据\u0026#34;） 不用 Web UI 更新 System Prompt # paperless-ai 的 Web UI 可以修改 system prompt，但改起来不方便。实际上 prompt 以 \\n 转义的单行格式存储在容器内 /app/data/.env 的 SYSTEM_PROMPT 字段（Docker volume 内）。\n注意： dotenv v16 在解析未加引号的值时，遇到 \\n 后跟 # 会将其视为注释截断，导致含有 ## 章节标题 的 prompt 被静默截断。解决方案：写入时用双引号包裹值。\n工作流：将 prompt 保存为 PROMPT.md（与 docker-compose.yml 同目录），用辅助脚本推送到容器：\n1 2 3 4 5 6 7 8 9 10 11 12 # update-prompt.sh — 编辑 PROMPT.md 后运行 docker cp PROMPT.md paperless-ai:/tmp/PROMPT.md docker exec paperless-ai node -e \u0026#34; const fs = require(\u0026#39;fs\u0026#39;), dotenv = require(\u0026#39;dotenv\u0026#39;); const prompt = fs.readFileSync(\u0026#39;/tmp/PROMPT.md\u0026#39;, \u0026#39;utf8\u0026#39;).trimEnd(); const escaped = prompt.replace(/\\\\\\\\/g,\u0026#39;\\\\\\\\\\\\\\\\\u0026#39;).replace(/\\\u0026#34;/g,\u0026#39;\\\\\\\\\\\u0026#34;\u0026#39;).replace(/\\n/g,\u0026#39;\\\\\\\\n\u0026#39;); let env = fs.readFileSync(\u0026#39;/app/data/.env\u0026#39;,\u0026#39;utf8\u0026#39;); fs.writeFileSync(\u0026#39;/app/data/.env\u0026#39;, env.replace(/^SYSTEM_PROMPT=.*$/m,\u0026#39;SYSTEM_PROMPT=\\\u0026#34;\u0026#39;+escaped+\u0026#39;\\\u0026#34;\u0026#39;)); const val = dotenv.parse(fs.readFileSync(\u0026#39;/app/data/.env\u0026#39;)).SYSTEM_PROMPT; console.log(\u0026#39;Updated:\u0026#39;, val?.length, \u0026#39;chars\u0026#39;); \u0026#34; docker restart paperless-ai 脚本位于 ~/repo/paperless-ai/update-prompt.sh，已设为可执行。\n","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/posts/paperless-ai-setup/","section":"文章","summary":"本文是一份完整的操作手册，介绍如何通过 paperless-ai 和本地运行的 Ollama 为 paperless-ngx 集成 AI 自动标签和分类功能。整套方案使用本地大语言模型读取文档文本，自动填充元数据字段——包括标题、文档类型、标签、联系人、日期以及自定义字段。\n硬件与架构 # NAS（群晖 DS1621+，10.0.10.10）：在 5656 端口运行 paperless-ngx 台式 PC：Windows，安装了 WSL2、Docker Desktop，配备 RTX 4090 目标：使用本地 LLM 实现 AI 自动打标/分类，零云端依赖 核心架构决策是拉取模式（pull model）：paperless-ai 运行在 WSL2 的 Docker 容器中，轮询 paperless-ngx API 寻找带有 ai-pending 标签的文档，调用 Ollama 处理后将元数据写回。对于不是 24 小时开机的台式机而言，这是最正确的方案——NAS 保存待处理队列，台式机开机后自动消费。\n1 2 3 4 5 6 7 paperless-ngx (NAS) ↑ ↓ (REST API) paperless-ai (WSL2 Docker) ↑ ↓ (HTTP) Ollama (Windows 原生) ↑ RTX 4090 (GPU) Ollama 以原生方式运行在 Windows 上（而非 WSL 内），以获得最佳 GPU 访问性能。在 WSL2 的 Docker 容器内，通过特殊主机名 host.docker.internal 访问 Ollama。\n","title":"使用 paperless-ai 与 Ollama 为 paperless-ngx 添加 AI 文档分类功能","type":"posts"},{"content":"如何将本地讲座/课程视频（没有 TMDB/TVDB 收录的）在 Plex 中整理成结构清晰的电视节目，并通过 Plex API 写入自定义描述文字。\n示例是 Jonathan Biss 的 Exploring Beethoven\u0026rsquo;s Piano Sonatas——Curtis 音乐学院与 Coursera 合作的 5 部分课程，存储在 Synology NAS 上。\n问题 # Plex 默认的 metadata 爬虫依赖 TMDB 或 TVDB。没有收录的本地讲座视频要么显示成一堆无封面的乱列表，要么被错误匹配到不相关的节目。\n解决方案：将课程当作**电视节目（TV Show）**处理，使用 Plex 的 Personal Media Shows 代理，再通过 Plex API 推送自定义 metadata。\n第一步：按 Season/Episode 结构整理文件 # Plex 的 TV Show 扫描器要求文件名包含 SxxExx：\n1 2 3 4 5 6 7 Exploring Beethoven\u0026#39;s Piano Sonatas/ ├── Season 01/ │ ├── Exploring Beethoven\u0026#39;s Piano Sonatas - S01E01 - Lecture 1, Part 1.mp4 │ ├── Exploring Beethoven\u0026#39;s Piano Sonatas - S01E01 - Lecture 1, Part 1.en.srt │ └── ... ├── Season 02/ └── ... 每个\u0026quot;讲\u0026quot;对应一个 Season，每个\u0026quot;Part\u0026quot;对应一集。字幕文件放在视频旁边，命名加 .en.srt 后缀，Plex 会自动识别为英文字幕。\n用一个 shell 脚本批量重命名和移动文件，支持 --dry-run 预览：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #!/bin/bash BASE=\u0026#34;/volume1/data/media/courses/Exploring Beethoven\u0026#39;s Piano Sonatas\u0026#34; SHOW=\u0026#34;Exploring Beethoven\u0026#39;s Piano Sonatas\u0026#34; DRY_RUN=false [[ \u0026#34;$1\u0026#34; == \u0026#34;--dry-run\u0026#34; ]] \u0026amp;\u0026amp; DRY_RUN=true for file in \u0026#34;$BASE\u0026#34;/*.mp4; do filename=$(basename \u0026#34;$file\u0026#34;) S=$(echo \u0026#34;$filename\u0026#34; | sed \u0026#39;s/^\\([0-9]*\\) - .*/\\1/\u0026#39;) E=$(echo \u0026#34;$filename\u0026#34; | sed \u0026#39;s/^[0-9]* - \\([0-9]*\\) - .*/\\1/\u0026#39;) TITLE=$(echo \u0026#34;$filename\u0026#34; | sed \u0026#39;s/^[0-9]* - [0-9]* - //\u0026#39; | sed \u0026#39;s/ ([^)]*)\\.[^.]*$//\u0026#39;) SS=$(printf \u0026#34;%02d\u0026#34; \u0026#34;$S\u0026#34;); EE=$(printf \u0026#34;%02d\u0026#34; \u0026#34;$E\u0026#34;) mkdir -p \u0026#34;$BASE/Season $SS\u0026#34; mv \u0026#34;$file\u0026#34; \u0026#34;$BASE/Season $SS/${SHOW} - S${SS}E${EE} - ${TITLE}.mp4\u0026#34; done # 将字幕从 srts/ 子文件夹移到对应 Season 目录 for file in \u0026#34;$BASE/srts\u0026#34;/*.srt; do filename=$(basename \u0026#34;$file\u0026#34;) S=$(echo \u0026#34;$filename\u0026#34; | sed \u0026#39;s/^\\([0-9]*\\) - .*/\\1/\u0026#39;) E=$(echo \u0026#34;$filename\u0026#34; | sed \u0026#39;s/^[0-9]* - \\([0-9]*\\) - .*/\\1/\u0026#39;) TITLE=$(echo \u0026#34;$filename\u0026#34; | sed \u0026#39;s/^[0-9]* - [0-9]* - //\u0026#39; | sed \u0026#39;s/ ([^)]*)\\.[^.]*$//\u0026#39;) SS=$(printf \u0026#34;%02d\u0026#34; \u0026#34;$S\u0026#34;); EE=$(printf \u0026#34;%02d\u0026#34; \u0026#34;$E\u0026#34;) mv \u0026#34;$file\u0026#34; \u0026#34;$BASE/Season $SS/${SHOW} - S${SS}E${EE} - ${TITLE}.en.srt\u0026#34; done 第二步：配置 Plex 资料库 # 在 Plex 中新建资料库：\n类型： 电视节目（TV Shows） 文件夹： 指向节目根目录（或 courses/ 父目录） 高级 → 代理： Personal Media Shows 高级： 勾选\u0026quot;优先使用本地 metadata\u0026quot; 保存后扫描，Plex 会自动从文件名识别出所有季和集。\n第三步：通过 Plex API 写入描述文字 # Personal Media Shows 不会从任何地方拉取描述。可以在 Plex UI 里手动编辑，但有多季时用 API 批量推送更方便。\n获取 Plex Token # Plex Web → 播放任意视频 → \u0026ldquo;\u0026hellip;\u0026rdquo; → \u0026ldquo;获取信息\u0026rdquo; → \u0026ldquo;查看 XML\u0026rdquo;，URL 中有 X-Plex-Token=XXXXX。\n核心 API 调用 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import urllib.request, urllib.parse, json BASE = \u0026#34;http://plex.nas:32400\u0026#34; TOKEN = \u0026#34;your-token-here\u0026#34; def plex_get(path, params=None): url = BASE + path if params: url += \u0026#34;?\u0026#34; + urllib.parse.urlencode(params) req = urllib.request.Request(url) req.add_header(\u0026#34;X-Plex-Token\u0026#34;, TOKEN) req.add_header(\u0026#34;Accept\u0026#34;, \u0026#34;application/json\u0026#34;) with urllib.request.urlopen(req) as r: return json.loads(r.read()) # 列出所有资料库 sections = plex_get(\u0026#34;/library/sections\u0026#34;) # 列出某个资料库的所有节目（key=7） shows = plex_get(\u0026#34;/library/sections/7/all\u0026#34;) # 更新节目描述（type=2）或季描述（type=3） def set_summary(section_key, rating_key, item_type, summary): params = { \u0026#34;type\u0026#34;: item_type, \u0026#34;id\u0026#34;: rating_key, \u0026#34;summary.value\u0026#34;: summary, \u0026#34;summary.locked\u0026#34;: 1, } url = f\u0026#34;{BASE}/library/sections/{section_key}/all?\u0026#34; + urllib.parse.urlencode(params) req = urllib.request.Request(url, method=\u0026#34;PUT\u0026#34;) req.add_header(\u0026#34;X-Plex-Token\u0026#34;, TOKEN) urllib.request.urlopen(req) summary.locked=1 可以防止 Plex 在下次刷新 metadata 时覆盖你写入的文字。\n完整脚本逻辑 # 脚本自动发现节目和所有季，然后逐一推送预先写好的描述：\n1 2 3 4 5 6 7 section_key, show_key = find_show(base_url, token, SHOW_TITLE) seasons = get_seasons(base_url, token, show_key) set_summary(section_key, show_key, 2, SHOW_SUMMARY) # 节目级描述 for season in seasons: idx = season[\u0026#34;index\u0026#34;] set_summary(section_key, season[\u0026#34;ratingKey\u0026#34;], 3, SEASON_SUMMARIES[idx]) 注意事项 # 如果 Plex 自动匹配了错误的节目，可以在 UI 中\u0026quot;修正匹配 → 无\u0026quot;，或将资料库代理切换为 Personal Media Shows 后重新扫描。 Plex 显示的节目标题可能和文件夹名不同（取决于之前匹配到了什么）。在搜索前先通过 API（/library/sections/{key}/all）确认实际标题。 海报图片：在节目根目录放一张 poster.jpg，Plex 会自动读取。 ","date":"2026年3月12日","externalUrl":null,"permalink":"/zh/posts/plex-local-media-metadata/","section":"文章","summary":"如何将本地讲座/课程视频（没有 TMDB/TVDB 收录的）在 Plex 中整理成结构清晰的电视节目，并通过 Plex API 写入自定义描述文字。\n示例是 Jonathan Biss 的 Exploring Beethoven’s Piano Sonatas——Curtis 音乐学院与 Coursera 合作的 5 部分课程，存储在 Synology NAS 上。\n问题 # Plex 默认的 metadata 爬虫依赖 TMDB 或 TVDB。没有收录的本地讲座视频要么显示成一堆无封面的乱列表，要么被错误匹配到不相关的节目。\n解决方案：将课程当作**电视节目（TV Show）**处理，使用 Plex 的 Personal Media Shows 代理，再通过 Plex API 推送自定义 metadata。\n第一步：按 Season/Episode 结构整理文件 # Plex 的 TV Show 扫描器要求文件名包含 SxxExx：\n","title":"在 Plex 中整理本地讲座视频并添加 Metadata","type":"posts"},{"content":"","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/tags/document-management/","section":"Tags","summary":"","title":"Document-Management","type":"tags"},{"content":"","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/tags/google-drive/","section":"Tags","summary":"","title":"Google-Drive","type":"tags"},{"content":"将近十年积累的个人文档从 Google Drive 文件夹体系迁移到 Paperless-ngx 的完整记录。 涵盖分类体系设计、从 Google Takeout 批量导入、ML 分类器训练，以及日常收件箱工作流。\n为什么要迁移 # 过去多年，我的\u0026quot;文档管理\u0026quot;是一棵手工维护的 Google Drive 文件夹树：\n1 2 3 4 5 6 7 8 9 10 10 - 文书材料/ 10 - 证件材料/身份证件/ 30 - 移民文档/ 30 - Tax Filing/ 40 - Finance/ 50 - 车辆注册/ 60 - 住房买房/ 80 - Medical/ 20 - 家装住房信息/ 80 - 旅行计划/ 归档时还算顺手，但检索很痛苦。想找\u0026quot;2022 年的保险表格\u0026quot;，要翻六个文件夹，还得猜当时的命名。 Paperless-ngx 提供全文检索、OCR、以及会从你自己的标注中学习的 ML 分类器—— 对于横跨移民手续、税务申报、房产合同、医疗记录的文档库来说，这是本质性的提升。\n系统架构 # 1 2 3 4 5 6 7 8 9 Google Takeout .zip │ migrate.py（分类 + 上传） ▼ Paperless-ngx（Docker，10.0.10.10:5656） │ REST API POST /api/documents/post_document/ │ OCR + 全文索引 │ ML 分类器（在已标注语料上重训练） ▼ NAS 存储（/volume1/docker/paperless/） Paperless-ngx 通过 Synology Container Manager 运行在 Docker 中。 所有存储（文档、数据库、Redis）挂载到 /volume1/docker/paperless/。\n分类体系设计 # 批量导入之前把分类体系设计好很重要——它是 ML 分类器的训练信号。 400 份文档贴错标签，等于教错了东西。\n文档类型 # 目标是互斥且完备的类型——覆盖我实际拥有的文档，不多不少。 全部用中文命名，ML 分类器不管标签是什么语言都能学。\nID 名称 归入此类的内容 1 发票收据 已付款的发票、收据、付款确认（仅限完成交易） 3 操作手册 产品说明书、用户指南、组装说明 4 活动通告 活动邀请函、公告（非预订类） 5 参考资料 参考材料、价目表、宣传册 6 设备信息 设备规格、序列号、保修记录 7 日程课表 周期性日历、课程表（如音乐课日历） 8 金融账单 银行/投资/券商账单 9 税务文件 报税表、W-2、1099、1098、HSA 10 身份证件 护照、驾照、身份证 11 合同协议 合同、协议、租约、施工授权书 12 医疗记录 病历、处方、化验报告 13 证明证书 证明信、公证、各类证书 14 移民文件 I-797、I-94、I-20、EAD、绿卡 15 签证申请 签证申请材料（美、中、加等） 16 工资单 工资条 17 房产文件 贷款、建筑许可、房产税、HOA 18 车辆文件 车辆租约、DMV、年检 28 行程预订 酒店/机票预订确认、活动门票、通行证、SNO-PARK 等许可证 29 报价估价 维修估价、施工报价、项目提案（付款前的询价阶段） 设计决策说明：\n发票收据 = 已付款交易：酒店确认单归 行程预订；维修估价归 报价估价；只有完成付款的文件归此类 行程预订 vs 发票收据：酒店确认单是预订凭证，不是收据；入住结账后的账单才是收据。门票和通行证也归此类 报价估价 vs 发票收据：未付款的估价/报价是采购前阶段；付款并开票后转为发票收据 设备信息 ≠ 参考资料：设备档案（序列号、保修）与参考材料（价目表、手册）结构不同，分开有意义 活动通告 ≠ 日程课表：前者是一次性通知，后者是反复查阅的参考文档 金融账单合并银行 + 投资：都是周期性账单，必要时用往来方（HSBC vs Vanguard）区分 税务文件涵盖所有税务文档：不需要在类型层面细分 W-2/1099/报税表，标签和往来方承担这个维度 移民文件 vs 签证申请：在途维持身份文件（I-797、EAD）与签证申请材料是不同的业务场景 医疗账单 → 发票收据 + #医疗 标签：医疗账单本质是收据，标签提供\u0026quot;医疗\u0026quot;维度，不需要单独的类型 标签 # 标签处理不属于文档类型的横切维度：\n标签 用途 #保险 保险相关 #医疗 医疗主题 #教育 教育相关 #财务 财务主题 #房产 房地产 #旅行 旅行 #车辆 车辆 #移民 移民主题 #签证 签证主题 #税务 税务主题 #待处理 收件箱 / 待审阅 #重要 重要、时效性强 #归档 已归档，无需操作 年份标签经过考虑后放弃。 最初计划给每份文档加 #2016 到 #2026 的年份标签。 反思后发现：年份标签给 ML 训练信号增加噪音，而\u0026quot;文档年份\u0026quot;自定义字段能更干净地覆盖这个需求 （可按数值过滤和排序，不占用标签云空间）。\n自定义字段 # ID 名称 类型 用途 1 Amount 浮点数 发票/账单金额 2 Bill Period 日期 账单周期截止日 3 到期日期 日期 文件到期日（证件、签证） 4 Document Year 整数 税务年份、历史文档所属年份 5 保单/账号 字符串 保单号、账号 往来方 # 设置了出现频率足以作为过滤条件的实体：\nID 名称 5 IRS 6 California FTB 7 Google LLC 8 USCIS 9 US Dept of State 10 Vanguard 11 HSBC 12 County of Santa Clara 导入前：关闭 ML 自动匹配 # 批量导入 400 份文档之前，先关闭所有文档类型、标签、往来方的 Auto/ML 匹配。 如果自动匹配在导入过程中处于开启状态，Paperless 可能用半训练的模型尝试重新分类， 覆盖你精心指定的元数据。\n1 2 3 4 5 6 7 8 9 10 11 12 # 关闭所有文档类型的匹配 curl -s \u0026#34;http://10.0.10.10:5656/api/document_types/?page_size=50\u0026#34; \\ -H \u0026#34;Authorization: Token YOUR_TOKEN\u0026#34; | jq \u0026#39;.results[] | .id\u0026#39; | \\ while read id; do curl -s -X PATCH \u0026#34;http://10.0.10.10:5656/api/document_types/$id/\u0026#34; \\ -H \u0026#34;Authorization: Token YOUR_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;matching_algorithm\u0026#34;: 0}\u0026#39; done # 标签和往来方同理 # matching_algorithm: 0=无, 1=任意, 2=全部, 3=字面, 4=正则, 6=Auto/ML 导入完成后，对需要 ML 建议的类型、标签、往来方重新设置 \u0026quot;matching_algorithm\u0026quot;: 6。\n例外：#待处理 应永久保持 0（无）。 它是由工作流指定的状态标签，不是内容类别—— ML 没有理由去猜测它。\n迁移脚本 # migrate.py 一次性完成分类和上传，直接读取 Google Takeout .zip 而无需全部解压。\n分类逻辑 # 每个文件通过按优先级排列的 if 链根据文件夹路径进行分类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def classify(rel_path: str) -\u0026gt; dict: p = rel_path filename = Path(p).name year = extract_year(Path(p).parts) # 从路径任意部分提取年份 corr = corr_from_filename(filename) # 从文件名关键词匹配往来方 if \u0026#34;10 - 身份证件\u0026#34; in p: return result(DOCTYPE[\u0026#34;身份证件\u0026#34;], [TAG[\u0026#34;移民\u0026#34;]]) if \u0026#34;30 - 移民文档\u0026#34; in p: return result(DOCTYPE[\u0026#34;移民文件\u0026#34;], [TAG[\u0026#34;移民\u0026#34;]]) if \u0026#34;30 - Tax Filing\u0026#34; in p: tax_tags = [TAG[\u0026#34;税务\u0026#34;]] if re.search(r\u0026#39;w[\\-\\s]?2\\b\u0026#39;, fl): return result(DOCTYPE[\u0026#34;税务文件\u0026#34;], tax_tags, corr or CORR[\u0026#34;Google LLC\u0026#34;]) if re.search(r\u0026#39;(f?1099|f?1098|f?5498|1095)\u0026#39;, fl): return result(DOCTYPE[\u0026#34;税务文件\u0026#34;], tax_tags, corr) return result(DOCTYPE[\u0026#34;税务文件\u0026#34;], tax_tags) # ... 更多规则 ... return result(None, [TAG[\u0026#34;待处理\u0026#34;]]) # 兜底：进收件箱 年份从路径中任意匹配 \\b(20[12]\\d)\\b 的部分提取—— 30 - Tax Filing/2022/W2.pdf 会自动得到 year=2022。\n往来方先从文件名关键词推断（google、hsbc、vanguard、irs、ftb）， 再由文件夹特定规则覆盖。\n上传 # 1 2 3 4 5 6 7 resp = requests.post( f\u0026#34;{API_BASE}/documents/post_document/\u0026#34;, headers={\u0026#34;Authorization\u0026#34;: f\u0026#34;Token {API_TOKEN}\u0026#34;}, files={\u0026#34;document\u0026#34;: (filename, data)}, data=form_data, # (key, value) 元组列表，支持重复字段 timeout=120, ) 标签必须以重复表单字段方式发送（不是 JSON 数组）：\n1 2 for tag_id in meta[\u0026#34;tags\u0026#34;]: form_data.append((\u0026#34;tags\u0026#34;, tag_id)) 自定义字段使用 JSON 编码的字典，键为字符串：\n1 form_data.append((\u0026#34;custom_fields\u0026#34;, json.dumps({str(YEAR_FIELD_ID): year}))) 文件类型过滤 # 跳过混入 Takeout 的非文档文件：\n1 2 3 4 5 6 SKIP_EXTENSIONS = { \u0026#34;.html\u0026#34;, \u0026#34;.csv\u0026#34;, \u0026#34;.qfx\u0026#34;, \u0026#34;.gsheet\u0026#34;, \u0026#34;.gdoc\u0026#34;, \u0026#34;.gslides\u0026#34;, \u0026#34;.gdraw\u0026#34;, \u0026#34;.gmap\u0026#34;, \u0026#34;.java\u0026#34;, \u0026#34;.bin\u0026#34;, \u0026#34;.db\u0026#34;, \u0026#34;.exe\u0026#34;, \u0026#34;.7z\u0026#34;, \u0026#34;.rar\u0026#34;, \u0026#34;.gshortcut\u0026#34;, \u0026#34;.pptx\u0026#34;, \u0026#34;.xls\u0026#34;, \u0026#34;.xlsx\u0026#34;, \u0026#34;.tar\u0026#34;, \u0026#34;.doc\u0026#34;, \u0026#34;.docx\u0026#34;, # Paperless OCR 支持不稳定 } .doc/.docx 技术上受 Paperless 支持，但 OCR 效果不稳定； 如果需要全文检索，建议先导出为 PDF。\n预演模式 # 1 python3 migrate.py --dry-run 输出示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ================================================================= IMPORT PREVIEW — 358 files to import ================================================================= By document type: 税务文件 89 files 移民文件 72 files 金融账单 61 files ... Skipped (non-document files): 47 Sample mappings (first 30): 10 - 文书材料/30 - Tax Filing/2022/W2-Google.pdf → 税务文件 | tags:[\u0026#39;税务\u0026#39;] | corr:Google LLC | year:2022 title: W2-Google 执行导入 # 1 python3 migrate.py --execute --yes --yes 跳过确认提示（通过 SSH 运行时 input() 会挂起，必须加此参数）。 Paperless 按内容哈希去重，重复运行是安全的。\n导入结果 # 358 份文档分布在 17 种文档类型中（后续分类体系扩展至 19 种，见下文）。含 OCR 处理时间，上传约耗时 25 分钟。 排除 .doc/.docx 后零错误。\nML 分类器训练 # 所有文档完成 OCR 且批量清除 #待处理 标签后：\n1 2 3 # 在 NAS 上运行（需要 docker 权限） /usr/local/bin/docker exec paperless-webserver-1 \\ python3 manage.py document_create_classifier ~400 份已标注文档为分类器提供了扎实的训练集。 最常见的文档类型（税务文件、移民文件、金融账单各有 50-90 个样本）效果较好。 较少见的类型随着文档增加会持续改善。\n训练前等 OCR 完成。 分类器在 OCR 文本上训练，不是原始文件。 过早运行意味着在空白或残缺的文本上训练。\n收件箱工作流 # 新文档（手动上传、移动端扫描、邮件导入）自动获得 #待处理 标签：\n设置 → 工作流 → 添加工作流：\n名称：新文档收件箱 触发：文档添加（类型 2） 动作：指定标签 #待处理（动作类型 1） 无论来源如何，每份新文档都会触发工作流，给你一个可靠的收件箱视图。\n#待处理 的 matching_algorithm 永久设为 0（无）——它只由工作流指定，ML 不会猜测它。 这保证了它作为状态信号的干净性。\n审阅完文档后移除 #待处理，文档从收件箱移出。\n日常使用方式 # 文档来源 # 来源 方式 纸质文档 手机扫描 App（如 Scanner Pro），上传到 Paperless 邮件附件 Paperless 邮件收件箱（IMAP 轮询，单独配置） 下载的 PDF 拖放到 Paperless UI 或消费文件夹 消费文件夹 NAS 上的 SMB 共享，从 Windows/Mac 可访问 审阅流程 # 打开保存的搜索：标签:#待处理 逐份文档：确认类型，补充缺失标签，修正标题 移除 #待处理——文档离开收件箱 对时效性强或即将到期的文档加 #重要 检索文档 # 全文搜索处理大多数场景（如\u0026quot;EAD renewal 2023\u0026quot;） 按文档类型 + 往来方过滤账单（金融账单 + Vanguard） 按往来方 + Document Year 自定义字段过滤税务文档 完整处理且无需后续操作的文档加 #归档 凭证备份 # 将 Paperless 管理员密码和 API Token 存入密码管理器（Bitwarden）。 API Token 在 设置 → Tokens 中查看；如需轮换，更新所有脚本中的引用。\n排障记录 # 批量清除 #待处理 标签 # 如果在禁用 Auto/ML 之前运行了分类器，它可能已经学会给所有文档打此标签。 通过 API 批量清除：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requests API_BASE = \u0026#34;http://10.0.10.10:5656/api\u0026#34; API_TOKEN = \u0026#34;YOUR_TOKEN\u0026#34; TAG_ID = 7 # #待处理 headers = {\u0026#34;Authorization\u0026#34;: f\u0026#34;Token {API_TOKEN}\u0026#34;, \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;} r = requests.get(f\u0026#34;{API_BASE}/documents/?tags__id__all={TAG_ID}\u0026amp;page_size=500\u0026#34;, headers=headers) doc_ids = [d[\u0026#34;id\u0026#34;] for d in r.json()[\u0026#34;results\u0026#34;]] print(f\u0026#34;待清理文档数：{len(doc_ids)}\u0026#34;) r = requests.post(f\u0026#34;{API_BASE}/documents/bulk_edit/\u0026#34;, headers=headers, json={\u0026#34;documents\u0026#34;: doc_ids, \u0026#34;method\u0026#34;: \u0026#34;remove_tag\u0026#34;, \u0026#34;parameters\u0026#34;: {\u0026#34;tag\u0026#34;: TAG_ID}}) print(r.status_code, r.text) 文档类型被错误自动指定 # ML 在语料不均衡或有噪音时可能分错。手动纠正后重新训练：\n1 2 /usr/local/bin/docker exec paperless-webserver-1 \\ python3 manage.py document_create_classifier 每次手动纠正都会反馈到训练集。\ncustom_fields 格式错误 # Paperless 期望自定义字段为以字符串为键的 JSON 编码字典：\n1 2 3 4 5 # ✓ 正确 json.dumps({\u0026#34;4\u0026#34;: 2024}) # ✗ 错误——返回 400 错误 json.dumps([{\u0026#34;field\u0026#34;: 4, \u0026#34;value\u0026#34;: 2024}]) 端口被占用 / 防火墙 # 如果 Paperless 不可达，检查 NAS 上 5656 端口是否有冲突服务。 DSM 防火墙也可能拦截特定子网的访问——检查 控制面板 → 安全性 → 防火墙。\n我的配置 # 项目 值 NAS Synology DS1621+ NAS IP 10.0.10.10 Paperless 端口 5656 Docker 镜像 ghcr.io/paperless-ngx/paperless-ngx:latest 导入文档数 358 导入来源 Google Takeout（单个 .zip，约 2 GB） OCR 语言 中文 + 英文 训练语料 ~400 份已标注文档，覆盖 19 种类型 备注 # post_document 接口是异步的——Paperless 将文档加入 OCR 队列后立即返回 {\u0026quot;result\u0026quot;: \u0026quot;OK\u0026quot;}，文档要等 OCR 完成后才可搜索（通常每份几秒到一分钟，取决于 NAS 负载） Paperless 按文件内容的 SHA256 去重——重复运行导入脚本是安全的，重复文档会被静默跳过 Google Takeout 默认导出 Google Docs/Sheets 为原生格式；如需 PDF， 创建 Takeout 时选择\u0026quot;导出格式: PDF\u0026quot; document_create_classifier 命令在训练集未变化时输出 No updates since last training——这是正常现象，不是错误 ","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/posts/paperless-ngx-migration/","section":"文章","summary":"将近十年积累的个人文档从 Google Drive 文件夹体系迁移到 Paperless-ngx 的完整记录。 涵盖分类体系设计、从 Google Takeout 批量导入、ML 分类器训练，以及日常收件箱工作流。\n为什么要迁移 # 过去多年，我的\"文档管理\"是一棵手工维护的 Google Drive 文件夹树：\n1 2 3 4 5 6 7 8 9 10 10 - 文书材料/ 10 - 证件材料/身份证件/ 30 - 移民文档/ 30 - Tax Filing/ 40 - Finance/ 50 - 车辆注册/ 60 - 住房买房/ 80 - Medical/ 20 - 家装住房信息/ 80 - 旅行计划/ 归档时还算顺手，但检索很痛苦。想找\"2022 年的保险表格\"，要翻六个文件夹，还得猜当时的命名。 Paperless-ngx 提供全文检索、OCR、以及会从你自己的标注中学习的 ML 分类器—— 对于横跨移民手续、税务申报、房产合同、医疗记录的文档库来说，这是本质性的提升。\n","title":"用 Paperless-ngx 整理十年文档：从 Google Drive 文件夹到全文检索归档库","type":"posts"},{"content":"","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/tags/airprint/","section":"Tags","summary":"","title":"Airprint","type":"tags"},{"content":"","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/tags/cups/","section":"Tags","summary":"","title":"Cups","type":"tags"},{"content":"","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/tags/printing/","section":"Tags","summary":"","title":"Printing","type":"tags"},{"content":"在 Synology NAS 上配置 AirPrint 的操作手册，让 iOS/macOS 设备可以通过局域网打印到 USB 或网络打印机。使用 Docker CUPS 容器和 Synology 内置的 avahi（mDNS）守护进程实现服务发现。\n架构 # 1 2 3 4 5 6 7 8 9 10 11 iPhone │ mDNS 发现 (_ipp._tcp) ▼ Synology avahi-daemon（eth4，端口 5353） │ 从 /etc/avahi/services/ 读取服务文件 │ CUPS Docker 容器（host 网络模式，端口 631） │ 生成 /etc/avahi/services/AirPrint-*.service │ 将打印任务代理到打印机 ▼ 打印机（如 socket://10.0.20.50:9100） 关键设计决策：\n容器使用 network_mode: host — mDNS 广播必须如此 容器直接挂载 /etc/avahi/services，让 Synology 的 avahi 读取其服务文件 必须禁用 Synology 自带的 CUPS，以释放端口 631 前置条件 # 已安装 Docker（Container Manager）的 Synology DSM NAS 的 SSH 访问权限 打印机可从 NAS 访问（USB 或网络 socket） 禁用 Synology 内置 CUPS 打印服务（通过 DSM 软件包或 synoservicectl） 配置步骤 # 1. 创建目录结构 # 1 mkdir -p /volume1/docker/cups/config 2. docker-compose.yaml # 创建 /volume1/docker/cups/docker-compose.yaml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 version: \u0026#39;3\u0026#39; services: cups-airprint: image: tigerj/cups-airprint:latest container_name: cups-airprint network_mode: \u0026#34;host\u0026#34; environment: - CUPSADMIN=admin - CUPSPASSWORD=yourpassword volumes: - /volume1/docker/cups/config:/config - /etc/avahi/services:/services # 让容器管理 avahi 服务文件 restart: unless-stopped 3. 启动容器 # 1 2 cd /volume1/docker/cups docker-compose up -d 4. 通过 CUPS Web UI 添加打印机 # 在浏览器中打开 http://\u0026lt;nas-ip\u0026gt;:631，用管理员凭据登录后添加打印机：\nAdministration → Add Printer 网络打印机：socket://10.0.x.x:9100 设置 Shared: Yes 选择合适的 PPD/驱动（Gutenprint 对大多数 Brother/HP 打印机兼容性好） 容器的 printer-update.sh 脚本会监听 CUPS 打印机变化，并自动重新生成 /etc/avahi/services/ 中的 avahi 服务文件。\n5. 验证 mDNS 广播 # 1 2 avahi-browse -a -t | grep -i airprint # 应显示：AirPrint \u0026lt;PrinterName\u0026gt; @ Synology _ipp._tcp local 6. 验证 CUPS 可访问 # 1 2 curl http://\u0026lt;nas-ip\u0026gt;:631/printers/ # 应列出你的打印机，状态为 Idle 排障记录 # 编辑 yaml 后容器使用了错误的卷挂载 # 编辑 docker-compose.yaml 修改 /services 挂载后，必须重新创建容器（而不仅仅是重启）才能生效：\n1 docker-compose down \u0026amp;\u0026amp; docker-compose up -d 验证当前挂载：\n1 2 docker inspect cups-airprint --format \u0026#39;{{json .Mounts}}\u0026#39; # /services 目标的 Source 应为 /etc/avahi/services iOS 上找不到打印机 # 按顺序检查：\navahi-browse -a -t | grep ipp — 服务是否正在广播？ curl http://nas-ip:631/printers/ — CUPS 是否在提供打印机服务？ 防火墙 — DSM 防火墙可能阻止设备子网访问端口 631 docker inspect cups-airprint — 确认 network_mode 为 host 强制 iOS 重新扫描：关闭/开启 Wi-Fi，或打开某 App 的打印对话框 avahi 只在一个接口广播 # 检查 /etc/avahi/avahi-daemon.conf — Synology 将 avahi 锁定到 allow-interfaces=eth4（主 LAN 口）。单网卡设置下这是正确的。如果你的 LAN 在其他接口，更新该行（但 DSM 升级后可能重置）。\n端口 631 已被占用 # Synology 的 CUPS 可能仍在运行。停止它：\n1 2 synoservicectl --stop cups synoservicectl --stop cups-lpd # 如有 我的配置 # 项目 值 NAS Synology DS1621+ NAS IP 10.0.10.10 打印机 Brother HL-2270DW 打印机 IP socket://10.0.20.50:9100 驱动 Brother HL-2250DN - CUPS+Gutenprint v5.2.11 Docker 镜像 tigerj/cups-airprint:latest avahi 接口 eth4 备注 # avahi 服务文件中的 URF=none TXT 记录对于使用传统 CUPS 驱动的老式打印机是正常现象 — iOS 仍然可以发现并使用该打印机 avahi 服务文件在容器启动和任意 CUPS 打印机状态变化时自动重新生成，无需手动编辑 docker-compose.yaml 是唯一事实来源 — 修改卷配置后始终重新创建（而非重启）容器 ","date":"2026年3月11日","externalUrl":null,"permalink":"/zh/posts/airprint-nas-setup/","section":"文章","summary":"在 Synology NAS 上配置 AirPrint 的操作手册，让 iOS/macOS 设备可以通过局域网打印到 USB 或网络打印机。使用 Docker CUPS 容器和 Synology 内置的 avahi（mDNS）守护进程实现服务发现。\n架构 # 1 2 3 4 5 6 7 8 9 10 11 iPhone │ mDNS 发现 (_ipp._tcp) ▼ Synology avahi-daemon（eth4，端口 5353） │ 从 /etc/avahi/services/ 读取服务文件 │ CUPS Docker 容器（host 网络模式，端口 631） │ 生成 /etc/avahi/services/AirPrint-*.service │ 将打印任务代理到打印机 ▼ 打印机（如 socket://10.0.20.50:9100） 关键设计决策：\n","title":"在 Synology NAS 上通过 CUPS Docker 实现 AirPrint","type":"posts"},{"content":"","date":"2026年3月9日","externalUrl":null,"permalink":"/zh/tags/immich/","section":"Tags","summary":"","title":"Immich","type":"tags"},{"content":"","date":"2026年3月9日","externalUrl":null,"permalink":"/zh/tags/photos/","section":"Tags","summary":"","title":"Photos","type":"tags"},{"content":"将家庭相册从 Synology Photos 迁移到自托管 Immich 实例的个人操作手册。 涵盖批量上传、Google Takeout 导入，以及通过 Synology PostgreSQL 数据库重建相册。\n环境说明 # 来源：运行 Synology Photos 的 Synology NAS（多用户） 目标：同一 NAS 上自托管的 Immich 上传工具：immich-go v0.31+ 客户端：Windows 上的 WSL2，SSH 访问 NAS 相册脚本：自定义 Python（migrate_albums.py），使用 Immich REST API 第一阶段：照片上传 # 策略 # 每位用户有两个来源：\nGoogle Takeout — 截止日期之前的照片（以 Google Photos 为主要存储时） Synology 文件夹 — 截止日期之后的照片（切换到 Synology 作为主存储后） 截止日期即你从 Google Photos 切换到 Synology 作为主要相册存储的时间点。此前的照片以完整分辨率存在 Google Photos 中；此后的原始分辨率在 Synology 上。\nimmich-go 文件夹上传 # 直接在 NAS 上运行 — 避免将大型相册（100 GB+）通过网络传输。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 ssh nas cd /path/to/immich-docker nohup ./immich-go upload from-folder \\ --server=http://\u0026lt;nas-ip\u0026gt;:2283 \\ --api-key=\u0026lt;user-api-key\u0026gt; \\ --concurrent-tasks=6 \\ --manage-raw-jpeg=StackCoverJPG \\ --manage-burst=Stack \\ --pause-immich-jobs=true \\ --session-tag \\ --on-errors=continue \\ /path/to/photos \\ \u0026gt; upload.log 2\u0026gt;\u0026amp;1 \u0026amp; 关键参数：\n参数 用途 --manage-raw-jpeg=StackCoverJPG 将 RAW+JPEG 对堆叠，以 JPEG 为封面 --manage-burst=Stack 堆叠连拍序列 --pause-immich-jobs=true 上传期间暂停 ML 任务（更快） --session-tag 用会话 ID 标记所有上传，便于追踪 --concurrent-tasks=6 并发数 — 根据 NAS CPU 调整 --on-errors=continue 单文件失败时不中止整体 将文件夹直接作为相册上传：\n1 2 3 4 5 6 ./immich-go upload from-folder \\ --server=http://\u0026lt;nas-ip\u0026gt;:2283 \\ --api-key=\u0026lt;user-api-key\u0026gt; \\ --into-album=\u0026#34;Album Name\u0026#34; \\ /path/to/folder \\ \u0026gt; upload.log 2\u0026gt;\u0026amp;1 \u0026amp; Google Takeout 导入 # 下载 Takeout 归档后：\n1 2 3 4 5 ./immich-go upload from-google-photos \\ --server=http://\u0026lt;nas-ip\u0026gt;:2283 \\ --api-key=\u0026lt;user-api-key\u0026gt; \\ --create-albums \\ /path/to/takeout-*.zip 使用 --date-range=\u0026lt;start\u0026gt;,\u0026lt;cutoff\u0026gt; 限制截止日期之前的照片（避免导入已在 Synology 上存有原始分辨率的 Google 压缩版本）。\n第二阶段：重建相册 # Synology Photos 将相册成员关系存储在 PostgreSQL 数据库中。照片上传到 Immich 后，通过读取 Synology 数据库 TSV 导出的脚本，利用 Immich REST API 重建相册。\n从 Synology 数据库导出相册数据 # Synology Photos 使用 PostgreSQL，数据库名为 synofoto。必须以 postgres 用户运行（peer 认证 — 无密码，但需要 sudo）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 在 NAS 终端执行： sudo su -s /bin/sh postgres -c \u0026#34;psql synofoto -A -F$\u0026#39;\\t\u0026#39; -t -c \\\u0026#34; SELECT a.id, a.name, a.shared, ui.name, u.id_user, ui2.name, f.name, u.filename, u.takentime, u.duplicate_hash FROM public.album a JOIN public.normal_album na ON na.id = a.id JOIN public.many_item_has_many_normal_album mia ON mia.id_normal_album = na.id JOIN public.item i ON i.id = mia.id_item JOIN public.unit u ON u.id_item = i.id JOIN public.folder f ON f.id = u.id_folder JOIN public.user_info ui ON ui.id = a.id_user JOIN public.user_info ui2 ON ui2.id = u.id_user ORDER BY a.id, f.name, u.filename; \\\u0026#34;\u0026#34; \u0026gt; /volume1/homes/\u0026lt;admin-user\u0026gt;/album_export.tsv # 复制到工作站： scp nas:/volume1/homes/\u0026lt;admin-user\u0026gt;/album_export.tsv ./album_export.tsv 注意： Synology 上的旧版 psql 不支持 --csv，使用 -A -F$'\\t' -t 输出 TSV 格式。\n关键数据表：\n表 用途 album 相册元数据（名称、所有者、共享标志） normal_album 标记为普通（非智能）类型的相册 many_item_has_many_normal_album 相册 ↔ 照片成员关系 item 照片条目（逻辑层，unit 的父级） unit 物理文件（文件名、拍摄时间、文件夹外键） folder 文件夹路径 user_info 用户显示名称 从数据库字段构建物理路径：\n个人空间：/volume1/homes/\u0026lt;username\u0026gt;/Photos\u0026lt;folder_path\u0026gt;/\u0026lt;filename\u0026gt; 共享空间：/volume1/photo\u0026lt;folder_path\u0026gt;/\u0026lt;filename\u0026gt; TSV 字段（10 列，制表符分隔）：\n1 album_id album_name shared owner file_owner_id file_owner folder_path filename takentime duplicate_hash 相册迁移脚本 # migrate_albums.py 读取 TSV 并在 Immich 中重建所有相册：\n对每张照片，调用 POST /api/search/metadata，参数为 originalFileName 如有多个文件名匹配，选择 takentime 最接近的（Synology 数据库的 Unix 时间戳） 以相册所有者账号通过 POST /api/albums 创建相册 通过 PUT /api/albums/{id}/assets 批量（每批 100 个）添加资产 通过 PUT /api/albums/{id}/users 将相册共享给家庭成员（editor 角色） 脚本顶部配置块：\n1 2 3 4 5 6 7 8 9 10 11 IMMICH_URL = \u0026#34;http://\u0026lt;nas-ip\u0026gt;:2283\u0026#34; API_KEYS = { \u0026#34;user1\u0026#34;: \u0026#34;\u0026lt;api-key\u0026gt;\u0026#34;, # 从 Immich UI → 头像 → 账户设置 → API Keys 获取 \u0026#34;user2\u0026#34;: \u0026#34;\u0026lt;api-key\u0026gt;\u0026#34;, } IMMICH_USER_IDS = { \u0026#34;user1\u0026#34;: \u0026#34;\u0026lt;immich-uuid\u0026gt;\u0026#34;, # 从 GET /api/users 或 Immich 管理面板获取 \u0026#34;user2\u0026#34;: \u0026#34;\u0026lt;immich-uuid\u0026gt;\u0026#34;, } 用法：\n1 2 3 4 5 6 7 # 预演 — 安全，不做任何修改 python3 migrate_albums.py --dry-run python3 migrate_albums.py --dry-run --album \u0026#34;My Album\u0026#34; # 执行 python3 migrate_albums.py --album \u0026#34;My Album\u0026#34; python3 migrate_albums.py 重要说明：\n脚本在创建前检查相册名称是否已存在 — 重复运行安全，但如果绕过检查会创建重复项 没有配置 API key 的照片所有者会被跳过并报告；补充 key 后重新运行即可 未匹配的照片（尚未上传）会被打印出来，但不阻塞其他相册的处理 搜索按 Immich 用户隔离，因此照片查找必须使用文件所有者的 API key，而非相册所有者的 验证 # 迁移完成后，对比 Synology 和 Immich 之间的相册照片数量： 在 Synology 数据库中查询每个相册的 COUNT(*)，与 GET /api/albums/{id} 返回的 assetCount 字段对比。\n经验总结 # 在 NAS 上运行 immich-go — 无网络瓶颈；对 100 GB+ 的相册库至关重要 上传期间暂停 Immich ML 任务 — --pause-immich-jobs=true 显著提升导入速度 immich-go 自动处理重复 — 重复运行安全；已上传的文件会被检测并跳过 每个 Immich 用户需要独立的 API key — 相册成员搜索必须使用文件所有者的 key（搜索结果按用户隔离） 文件名 + 拍摄时间匹配可靠 — originalFileName 搜索加 Unix 时间戳消歧，对 Synology Photos 数据集效果很好 Synology 上的旧版 psql — 没有 --csv 参数；使用 -A -F$'\\t' -t 输出 TSV immich-go 的 --session-tag — 用会话 ID 标记所有上传，便于审计哪些文件来自哪次运行 相册脚本幂等性 — 创建前检查已有相册名称；未匹配的照片不阻塞其他处理 参考资料 # Immich API 文档 immich-go 文档 ","date":"2026年3月9日","externalUrl":null,"permalink":"/zh/posts/synology-to-immich-migration/","section":"文章","summary":"将家庭相册从 Synology Photos 迁移到自托管 Immich 实例的个人操作手册。 涵盖批量上传、Google Takeout 导入，以及通过 Synology PostgreSQL 数据库重建相册。\n环境说明 # 来源：运行 Synology Photos 的 Synology NAS（多用户） 目标：同一 NAS 上自托管的 Immich 上传工具：immich-go v0.31+ 客户端：Windows 上的 WSL2，SSH 访问 NAS 相册脚本：自定义 Python（migrate_albums.py），使用 Immich REST API 第一阶段：照片上传 # 策略 # 每位用户有两个来源：\n","title":"Synology Photos 迁移到 Immich 操作手册","type":"posts"},{"content":" 四个月睡眠倒退期 # 宝宝进入包含5个不同阶段的新睡眠周期：\nREM（快速眼动）：做梦睡眠 第一阶段：非常浅/昏昏欲睡的睡眠 第二阶段：浅睡眠 第三/四阶段：深度恢复性睡眠 宝宝在整个夜晚每隔一两个小时就会进入浅睡眠状态。他们需要学会在浅睡/短暂清醒后自行重新入睡。\n七步指南\n加满油箱 检查引擎 设定巡航控制 再次补充油量 踩下刹车 规划休息站 卸下包袱 1. 加满油箱 # 白天保持充足的喂奶量。\n生长突增期很常见，我们需要根据饥饿信号来喂奶。如果白天喂得不够，宝宝在夜间就需要补充热量。我们要防止\u0026quot;反向循环\u0026quot;：\n夜间频繁喂奶 --\u0026gt; 白天吃奶不好 --\u0026gt; 夜间需要更多喂奶 2. 检查引擎 # 审查夜间作息流程\n给宝宝独立的睡眠空间\n创造有利于入睡的环境\n遮蔽光线 使用白噪音机 保持室温在68-78华氏度（20-26摄氏度） 建立可预期的睡前作息\n目标就寝时间为晚上7-8点\n3. 设定巡航控制 # 让宝宝自行入睡\nS.I.T.B.A.C.K. 方法 # 干预级别由低到高：\nS：Stop——停下来，等待、观察 I：Increase——提高白噪音机音量 T：Touch——轻触宝宝胸部 B：Binky——提供安抚奶嘴 A：Add——加入轻轻摇晃宝宝身体 C：Cuddle——抱起宝宝轻柔摇晃 K：喂奶时间到 3-4个月的策略 # 这个月龄的宝宝需要的支持超出了SITBACK方法本身。我们可以尝试逐步降低干预级别。如果宝宝在10-15分钟后仍无法入睡，则回退并提高干预级别。\n干预方式（由高到低） 喂奶、摇晃或弹跳直至完全入睡，再放入婴儿床 喂奶至入睡，放入婴儿床前轻轻_唤醒_他一点点 抱在臂弯中摇晃至入睡，放入婴儿床前轻轻_唤醒_一点点 宝宝在婴儿床中时，左右摇晃他的身体直至入睡 宝宝在婴儿床中时，将手放在宝宝胸部直至入睡 用摇晃或手放胸部安抚宝宝，在宝宝入睡_前_撤手离开 将宝宝清醒地放入床中，允许他哭闹5分钟，进去抱起直至平静，再次放下，重复循环 将宝宝清醒地放入床中，只在宝宝大哭时才介入 将宝宝清醒地放入床中，允许他在无干预的情况下自行入睡 4. 再次补充油量 # 考虑梦中喂奶\n在晚上9:30到11点之间进行。\n5. 踩下刹车 # 计划减少夜醒\n给几分钟时间让宝宝自行重新入睡 使用SITBACK方法 避免\u0026quot;快捷解决方案\u0026quot;，例如摇晃、喂奶等 尝试对其中一次夜醒使用SITBACK，其余夜醒使用\u0026quot;快捷解决方案\u0026quot;。\n四个月宝宝的SITBACK # 不要跳过步骤或匆忙进行。\n步骤 时间 S：停下来，等待、观察 5-8分钟 I：提高白噪音机音量 1-2分钟 T：轻触宝宝胸部 2分钟 B：提供安抚奶嘴 2分钟 A：轻轻摇晃宝宝身体，让头部轻柔晃动 2分钟 C：抱起宝宝轻柔摇晃 直至平静，或2分钟后进入K步骤 K：喂奶时间到 6. 规划休息站 # 处理白天小睡\n3.5到4.5小时，不超过5小时\n关注清醒窗口期（90-120分钟） 营造环境：尽可能保持黑暗 通过作息帮助宝宝做好准备 专注于第一次小睡，它是最容易建立的 ","date":"2024年5月16日","externalUrl":null,"permalink":"/zh/posts/parenting/sleep-training-baby/","section":"文章","summary":"四个月睡眠倒退期 # 宝宝进入包含5个不同阶段的新睡眠周期：\nREM（快速眼动）：做梦睡眠 第一阶段：非常浅/昏昏欲睡的睡眠 第二阶段：浅睡眠 第三/四阶段：深度恢复性睡眠 宝宝在整个夜晚每隔一两个小时就会进入浅睡眠状态。他们需要学会在浅睡/短暂清醒后自行重新入睡。\n七步指南\n加满油箱 检查引擎 设定巡航控制 再次补充油量 踩下刹车 规划休息站 卸下包袱 1. 加满油箱 # 白天保持充足的喂奶量。\n生长突增期很常见，我们需要根据饥饿信号来喂奶。如果白天喂得不够，宝宝在夜间就需要补充热量。我们要防止\"反向循环\"：\n夜间频繁喂奶 --\u003e 白天吃奶不好 --\u003e 夜间需要更多喂奶 2. 检查引擎 # 审查夜间作息流程\n给宝宝独立的睡眠空间\n创造有利于入睡的环境\n","title":"3-4个月宝宝的睡眠训练","type":"posts"},{"content":"","date":"2024年5月16日","externalUrl":null,"permalink":"/zh/tags/newborn/","section":"Tags","summary":"","title":"Newborn","type":"tags"},{"content":"","date":"2024年5月16日","externalUrl":null,"permalink":"/zh/categories/parenting/","section":"Categories","summary":"","title":"Parenting","type":"categories"},{"content":"","date":"2024年5月16日","externalUrl":null,"permalink":"/zh/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"2024年5月16日","externalUrl":null,"permalink":"/zh/tags/sleep-training/","section":"Tags","summary":"","title":"Sleep Training","type":"tags"},{"content":"","date":"2024年5月16日","externalUrl":null,"permalink":"/zh/series/sleep-training/","section":"Series","summary":"","title":"Sleep-Training","type":"series"},{"content":"","date":"2024年5月11日","externalUrl":null,"permalink":"/zh/tags/toddler/","section":"Tags","summary":"","title":"Toddler","type":"tags"},{"content":" 为小家伙做好准备 # B-E-S-T # B - Building confidence（建立自信）：聊聊关于睡觉的\u0026quot;好处\u0026quot;。可以表扬他们睡得很好，或者跟他们说自己睡得好、变强壮了，etc. 产生正面影响\nE - Expectations（明确期望）：明确表达你对他们的期望，希望他们做什么。\u0026ldquo;I\u0026rsquo;m going to give you lots of kisses, and you\u0026rsquo;re going to lay down on your bed, close your eyes and have a good sleep.\u0026rdquo;\nS - Simulate（模拟练习）：练习睡前流程。\u0026ldquo;show me how to close your eyes?\u0026rdquo;; talk to stuffed animals.\nT - Tanks（填满油箱）：睡前填满三个油箱。\n道晚安之前 # 白天影响夜晚 # 时间安排 # 晚上7-8点就寝最佳，但如果孩子睡得好，其他时间也可以。\n平均需要20-30分钟才能入睡。我们需要有意识地安排好一天的\u0026quot;收尾\u0026quot;。\n睡前作息开始前约30分钟：\n避免看屏幕。\n观察孩子的行为，留意困倦信号。\n过度疲劳的迹象：\n精力突然大幅上升 烦躁不安 动作笨拙 亲子连接有助于配合\n睡前作息 # 当孩子拒绝开始睡前作息时，使用SAD方法应对。\n技巧 # 15-20分钟 给予全神贯注的陪伴；兄弟姐妹可能需要一起参与 提前预判孩子可能提出的要求 让睡前作息愉快且有趣 不要催促 确保孩子已做好B-E-S-T的准备 亲吻安抚玩偶或毛绒玩具 睡前绘本：帮助孩子进行视觉化想象\n睡前图表：直观提示下一步该做什么\n道晚安之后 # 和你互动是他们的目标，尽量让互动变得无聊。\n你嘴里说出的话越少越好，只有两种类型：\n安抚性语言 # 简短，给予孩子支持。包含三个部分：\n事实：妈妈在这里\n肯定：你是安全的，你是被爱的\n身体期望与原因：闭上眼睛，闭上嘴巴。你的身体需要睡眠。\n\u0026ldquo;close your eyes\u0026rdquo;（闭上眼睛）优于\u0026quot;go to sleep\u0026quot;（去睡觉）：让指令具体可操作。\n你的版本：\n肯定语：\u0026ldquo;you\u0026rsquo;re okay\u0026rdquo; / \u0026ldquo;I believe in you\u0026rdquo; / \u0026ldquo;you\u0026rsquo;re trying so hard\u0026rdquo;\n引导性语言 # 谨慎使用，仅在需要\u0026quot;降温\u0026quot;时使用。\n不要用喊叫、愤怒、沮丧来\u0026quot;奖励\u0026quot;孩子。保持无聊。\n示例：\n目标：该睡觉了\n身体期望：你需要躺在床上并闭上眼睛\n警告：如果你不这样做，我需要离开。\n循序渐进法 # 每一行停留2个晚上，然后向下推进以取得进展。\n方案 # 需要安抚 爬出床外 情绪升温 重复安抚性语言并给予轻柔抚触 送回床边并说\u0026quot;待在床上，闭上眼睛\u0026quot; 说引导性语言，必要时离开房间。 在30秒、3分钟、5分钟、10分钟后给予Bedtime Boosts1 送回床边并说\u0026quot;待在床上，闭上眼睛\u0026quot;。如需要可增加Bedtime Boost频率。 说引导性语言，必要时离开房间。孩子配合后恢复Bedtime Boosts。 Bedtime Boosts（睡前鼓励） # 离开前说：\u0026ldquo;I\u0026rsquo;m going to leave. Shut your mouth and your eyes. I\u0026rsquo;ll come back to check in 30 seconds\u0026rdquo; 如果他乖乖留在床上，回来说安抚性语言，加上\u0026quot;I\u0026rsquo;ll go to the bathroom and come back to check on you if you stay quietly in bed.\u0026quot;（任何他们知道不会太久的活动） 技巧 # 循序渐进取得进步，不要过于激进。每一行至少停留2个晚上，也可以3个。\n父母双方共同参与\n半夜醒来：送孩子回床，使用安抚性语言 + Bedtime Boosts\n处理凌晨4-6点 # 如果孩子在6点前醒来，当作夜间处理，参见技巧部分的半夜醒来。\n过了早上6点后，切换到_白天_模式：\n开灯，拉开窗帘 \u0026ldquo;早上好！\u0026rdquo; 开始新的一天 早上好 # 让派对开始吧！\n描述孩子做得好的地方以强化行为：\u0026ldquo;You stayed in your bed the whole night!\u0026rdquo; / \u0026ldquo;Hooray!\u0026rdquo; / \u0026ldquo;sleeping is making me strong, can I see your muscles? Sleep is making your body strong too\u0026rdquo; 可能会遇到的情况：\n如果孩子醒来情绪不好：用你的热情表现来影响他们的情绪，让他们感受到早晨是美好的。\n如果孩子不喜欢一上来就\u0026quot;兴奋模式\u0026quot;：给他们时间慢慢醒来，但要明确划清睡眠和白天的界限。\n小睡 # 小睡准备 # 将房间布置得与夜晚完全一样，保持同样的黑暗环境。\n小睡的最佳时机：\n小睡前约6小时的清醒窗口 小睡后约5小时的清醒窗口 这个年龄的清醒窗口可以稍有弹性：清醒/小睡/就寝时间前后约30分钟的范围\n提供类似睡前的小睡作息：读书等，帮助孩子放松下来。\n小睡的循序渐进法 # 同样的方案：将夜间所做的事镜像到小睡中。\n如果留在房间不可行或效果不好？可以直接跳到方案底部（离开房间，使用Bedtime Boosts）。\n尝试小睡75-90分钟。到90分钟时结束小睡时间。\n如果没有睡着 # 补救小睡：提供20-30分钟的\u0026quot;补救小睡\u0026quot;机会，让孩子\u0026quot;意外\u0026quot;睡着，比如短途坐车等。在下午4点前进行。\n如果\u0026quot;补救小睡\u0026quot;没有发生，提前就寝时间。\n短暂小睡 # 短暂小睡：2岁以下少于90分钟、3岁以上少于60分钟的小睡\n在进入房间前给孩子15-25分钟重新入睡的时间。如果无法重新入睡，结束小睡时间。\n评估短暂小睡持续发生的原因：\n环境 小睡前的活动 清醒窗口 孩子对小睡的实际需求 睡眠训练之后 # 不要过度纠结于严格遵守作息和时间表：孩子有弹性，能够灵活应变，也能够适应变化。让孩子在生活中茁壮成长，学会\u0026quot;伸展\u0026quot;。\n重回正轨 # 因为某些事件（旅行等）导致作息被打乱。\n给孩子几天的缓冲期慢慢回归。 言出必行，让孩子知道没有\u0026quot;替代方案\u0026quot;。 用B-E-S-T为孩子做好准备。 检查睡前作息。 回到\u0026quot;循序渐进\u0026quot;方法。 保持日常的一致性。 额外注意事项 # 迎接新成员 # 避免在新生儿到来前后3个月内发生重大生活变化，以减少压力。\n针对幼儿要做的三件事：\n如果可以，多带孩子去户外：接触阳光和新鲜空气。 填满关注油箱：每天安排15分钟与幼儿面对面的专属时间（\u0026ldquo;特别时光\u0026rdquo;），让孩子主导。 保持一致的睡前作息。 睡觉时间大便 # 吃完饭后给30分钟的玩耍时间。 在睡前作息开始前给孩子私人时间，让他们在就寝前排便。 从婴儿床过渡到大床 # 强烈建议让孩子继续睡婴儿床。\nℹ️ Bedtime Boosts A bedtime boost is a way to offer reassurance while rewarding positive behavior. \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024年5月11日","externalUrl":null,"permalink":"/zh/posts/parenting/sleep-training-toddler/","section":"文章","summary":"为小家伙做好准备 # B-E-S-T # B - Building confidence（建立自信）：聊聊关于睡觉的\"好处\"。可以表扬他们睡得很好，或者跟他们说自己睡得好、变强壮了，etc. 产生正面影响\nE - Expectations（明确期望）：明确表达你对他们的期望，希望他们做什么。“I’m going to give you lots of kisses, and you’re going to lay down on your bed, close your eyes and have a good sleep.”\nS - Simulate（模拟练习）：练习睡前流程。“show me how to close your eyes?”; talk to stuffed animals.\n","title":"幼儿睡眠训练","type":"posts"},{"content":"与 Vim 相比，Neovim 内置了许多实用插件和功能，Lua 配置也更易读、更强大。以下是一些在 Nvim 中编写代码的实用技巧。\n搜索（文件、文本、诊断、帮助） # 与 search-in-vim 中介绍的技巧类似，我们可以在文件中搜索文本，或通过过滤文件名来查找文件。在 Neovim 中，我们使用 fzf-lua，与 fzf.vim 非常相似。\n使用 FZF 选择器\nFZF 的界面由一个选择器（对条目列表进行模糊搜索）和一个预览窗口组成。在选择器中，使用 c-j/k/n/p（或 c-u/d）移动。Tab/S-Tab 用于选择文件。默认操作（Enter）是编辑文件或发送到 qflist，具体取决于选中数量。若要编辑多个文件，使用 c-e。\n另一个实用命令是\u0026quot;恢复\u0026quot;（\u0026lt;Leader\u0026gt;sr），可以继续上次的 FZF 搜索。\n模糊匹配查找文件 # 用途 快捷键 说明 当前目录 中的文件 \u0026lt;Leader\u0026gt;f PWD 最近文件（MRU） \u0026lt;Leader\u0026gt;o 非常适合搜索之前打开过的文件 缓冲区 的同级文件 \u0026lt;Leader\u0026gt;s. 也包含子目录中的文件 项目范围的文件（git） \u0026lt;Leader\u0026gt;gf 只显示 git 跟踪的文件 以 缓冲区目录 为起点的任意路径 \u0026lt;Leader\u0026gt;sf 按 Enter 前可以修改路径 搜索文本 # 在文件中 Grep # 用途 快捷键 说明 当前目录中的文件 \u0026lt;leader\u0026gt;/ 使用 keyword -- glob 过滤文件（! 表示排除模式） Git 根目录 \u0026lt;leader\u0026gt;g/ 同上 当前 word/WORD \u0026lt;leader\u0026gt;w/W w 在可视模式下也有效 实时 grep：两个快捷键都使用\u0026quot;实时 grep\u0026quot;，即每次按键都会运行一条新的 ripgrep 命令并更新结果，可以即时测试 grep 表达式。\nglob：在实时 grep 中，可以在关键词后加 -- 来添加 glob。例如：-- *.lua !*.md lib/**/*.c。\n* 表示任意字符 ** 表示任意文件夹（递归） ! 表示排除匹配 模糊搜索缓冲区行 # 用途 快捷键 说明 搜索当前缓冲区的行 \u0026lt;leader\u0026gt;sb [S]earch [B]uffer lines 搜索所有已打开缓冲区 \u0026lt;leader\u0026gt;so [S]earch [O]pen buffer lines 搜索 Git 仓库内容 # 用途 快捷键 说明 搜索包含当前缓冲区的提交 \u0026lt;leader\u0026gt;gb [G]it [B]uffer commits 搜索所有提交 \u0026lt;leader\u0026gt;gc [G]it [C]ommits 搜索 Vim 帮助 # FZF 也非常适合搜索帮助标签和快捷键（通过 \u0026lt;leader\u0026gt; 后接 sh 和 sk）。还有一个用于查看 Man 手册的快捷键 \u0026lt;leader\u0026gt;sm。\nTreesitter # Treesitter 将文件解析为代码对象，不仅能提供更好的_高亮_，还带来了一套全新的动作对象，可在操作待定模式和可视模式中使用，使文本操作更加轻松和精确。\nTreesitter 对象 # 定义了以下字母对应的文本对象：\nc：类（Classes） m：方法/函数定义 f：函数调用 i：条件（if）语句 l：循环 a：参数 =：赋值 所有对象都支持通过 [ 和 ] 跳转到上一个/下一个。大写字母跳转到末尾，小写字母跳转到开头：\n[c：跳转到上一个类的开头 ]L：跳转到下一个循环的末尾 [f：跳转到上一个函数调用的开头 在操作待定模式下的示例：\ncr=：修改赋值右侧 caa：修改一个参数 cii：修改 if 内部（根据光标位置选中条件或条件体） daf：删除一个函数调用 Flash 插件快速选择代码块，通过 S 或 R 触发：\nS：从当前光标位置选择一个代码块 R：远程选择代码块 [x 跳转到上层作用域的开头。\n大纲 # 安装了 aerial.nvim 来显示 Treesitter 提供的精确文档大纲：\n切换侧边大纲窗口：\u0026lt;leader\u0026gt;a（\u0026lt;leader\u0026gt;A 停留在大纲窗口） 跳转到上一个/下一个大纲条目：\u0026lt;leader\u0026gt;{ 和 \u0026lt;leader\u0026gt;} LSP 与诊断 # Neovim 原生支持 LSP，提供_诊断_、签名帮助、文档、_代码操作_和_格式化_等功能。\n诊断 # 用途 快捷键 说明 上一个诊断 [d 下一个诊断 ]d 切换诊断显示 \u0026lt;leader\u0026gt;dt 切换虚拟文本显示 显示浮动框 \u0026lt;leader\u0026gt;df 显示包含详细信息的浮动框 Fzf 搜索诊断 \u0026lt;leader\u0026gt;sd 搜索当前文档的诊断 发送到 quickfix 列表 \u0026lt;leader\u0026gt;dq 之后可使用 :cdo 或 :cfdo 处理 发送到 location 列表 \u0026lt;leader\u0026gt;dl 使用 trouble.nvim # 用途 快捷键 缓冲区诊断 \u0026lt;leader\u0026gt;xX 工作区诊断 \u0026lt;leader\u0026gt;xx Quickfix 列表 \u0026lt;leader\u0026gt;xq 重命名与代码操作 # 用途 快捷键 重命名 \u0026lt;leader\u0026gt;rn 代码操作 \u0026lt;leader\u0026gt;ca 自动补全 # 快捷键 # 用途 快捷键 说明 上一个/下一个 \u0026lt;C-j\u0026gt; / \u0026lt;C-k\u0026gt; 关闭 \u0026lt;C-c\u0026gt; 滚动 \u0026lt;C-b\u0026gt; / \u0026lt;C-f\u0026gt; 确认 \u0026lt;C-y\u0026gt; 不是 CR 或 TAB 移动到上一个/下一个位置 \u0026lt;C-h\u0026gt; / \u0026lt;C-l\u0026gt; INSERT 模式下有效，适用于代码片段 下一个选项 \u0026lt;C-e\u0026gt; 用于代码片段中的选项节点 代码片段 # Luasnip 提供代码片段。我的代码片段位于 dotfiles/nvim/lua/snippets/，文件名需要与文件类型匹配。\n辅助代码片段（在 lua 中触发）：\nsnipf：用于带有 i 节点等的复杂代码片段 snipt：用于简单文本代码片段 Markdown 代码片段：sc 用于 Hugo 短代码。\n格式化 # 自动格式化 # 使用 conform.nvim 格式化文件：\n用途 快捷键 说明 手动触发格式化 ,f 调用 Conform 格式化_嵌入_代码 ,mf 如 Markdown 中的代码块 Prettierd 配置以 80 列宽格式化 markdown（需在 .config/nvim/utils/linter-config/.prettierd.toml 中设置 proseWrap = \u0026quot;always\u0026quot;） 使用 leap 和 grapple 进行跳转 # Leap 跳转 # 以 s 开头，然后输入两个字母即可跳转到可视范围内的任意位置。gs 跳转到另一个窗口。\n特殊字符：\u0026lt;space\u0026gt; 行尾，\u0026lt;space\u0026gt;\u0026lt;space\u0026gt; 空行，所有括号类型视为等价。\n远程操作 # 在操作待定模式下按 r 触发远程操作，无需移动光标即可对远程位置执行操作：\n按 y，再按 r 进入远程模式 输入远程位置的字母，按下标签键 输入动作，操作在远程文本上完成，光标回到原处 语法选择（flash） # 功能 快捷键 选择包含当前光标的代码块 S 远程选择代码块 R 文件间跳转（Grapple） # 用途 快捷键 说明 添加标签 \u0026lt;Leader\u0026gt;ma 可以输入可选名称 删除标签 \u0026lt;Leader\u0026gt;md 取消标记当前文件 移动到下一个标签 \u0026lt;Leader\u0026gt;n 显示所有标签并选择 \u0026lt;Leader\u0026gt;mk 文件管理 # oil.nvim 允许像编辑 vim 缓冲区一样编辑文件系统，保存时应用到文件系统。通过 nvim . 或 :e . 触发。\n使用 Git # gitsigns：行号栏差异标记，提供暂存/取消暂存辅助，以及内联差异、blame 等 Fugitive：经典插件 内联差异与暂存 # 用途 快捷键 说明 在变更/块之间移动 [h / ]h 预览块差异 \u0026lt;Leader\u0026gt;hp 对比差异（并排） \u0026lt;Leader\u0026gt;hd / \u0026lt;Leader\u0026gt;hD d：与已暂存版本；D：与最后一次提交 退出差异视图 \u0026lt;Leader\u0026gt;hq 暂存缓冲区/块 \u0026lt;Leader\u0026gt;hS/hs 大写对应缓冲区，小写对应块 重置缓冲区/块 \u0026lt;Leader\u0026gt;hR/hr 取消暂存块 \u0026lt;Leader\u0026gt;hu Blame 行 \u0026lt;Leader\u0026gt;hb 切换 blame 内嵌显示 \u0026lt;Leader\u0026gt;htb 切换已删除块的显示 \u0026lt;Leader\u0026gt;htd 暂存文件与创建提交 # 通过 Fugitive 的 status 视图：\n\u0026lt;leader\u0026gt;gs 打开 git status (、) 在文件之间移动 = 切换光标所在文件的差异显示 - 切换暂存状态 X 硬重置文件 cc 创建提交 杂项 # 预览 Markdown 文件 # ","date":"2024年5月7日","externalUrl":null,"permalink":"/zh/posts/coding/neovim-workflow/","section":"文章","summary":"与 Vim 相比，Neovim 内置了许多实用插件和功能，Lua 配置也更易读、更强大。以下是一些在 Nvim 中编写代码的实用技巧。\n搜索（文件、文本、诊断、帮助） # 与 search-in-vim 中介绍的技巧类似，我们可以在文件中搜索文本，或通过过滤文件名来查找文件。在 Neovim 中，我们使用 fzf-lua，与 fzf.vim 非常相似。\n使用 FZF 选择器\nFZF 的界面由一个选择器（对条目列表进行模糊搜索）和一个预览窗口组成。在选择器中，使用 c-j/k/n/p（或 c-u/d）移动。Tab/S-Tab 用于选择文件。默认操作（Enter）是编辑文件或发送到 qflist，具体取决于选中数量。若要编辑多个文件，使用 c-e。\n另一个实用命令是\"恢复\"（\u003cLeader\u003esr），可以继续上次的 FZF 搜索。\n模糊匹配查找文件 # 用途 快捷键 说明 当前目录 中的文件 \u003cLeader\u003ef PWD 最近文件（MRU） \u003cLeader\u003eo 非常适合搜索之前打开过的文件 缓冲区 的同级文件 \u003cLeader\u003es. 也包含子目录中的文件 项目范围的文件（git） \u003cLeader\u003egf 只显示 git 跟踪的文件 以 缓冲区目录 为起点的任意路径 \u003cLeader\u003esf 按 Enter 前可以修改路径 搜索文本 # 在文件中 Grep # 用途 快捷键 说明 当前目录中的文件 \u003cleader\u003e/ 使用 keyword -- glob 过滤文件（! 表示排除模式） Git 根目录 \u003cleader\u003eg/ 同上 当前 word/WORD \u003cleader\u003ew/W w 在可视模式下也有效 实时 grep：两个快捷键都使用\"实时 grep\"，即每次按键都会运行一条新的 ripgrep 命令并更新结果，可以即时测试 grep 表达式。\n","title":"Neovim 工作流","type":"posts"},{"content":"搜索是开发中最常用也是最重要的操作之一。Vim 提供了非常高效和方便的搜索功能。 这篇笔记记录了一些常用的搜索命令、例子以及自定义的 key mapping。\n使用 ripgrep 搜索文件内容（:Rg2 或 ,gg，,gw/,gW） 使用 git-grep 搜索 git branch/commit，以及使用 fzf 显示 git grep 的结果（:Ggrep） 使用 fzf/coc list 搜索当前文件/buffers 中的行，实现快速定位/跳转 使用 quickfix lists 快速访问上述搜索的结果，以及利用 :cdo/:cfdo 等命令对结果进行批量操作 在文件中通用搜索 # Ripgrep 与 fzf # fzf.vim 提供了 :Rg 命令来调用 ripgrep 搜索当前目录。不过，我构建了一个更实用的变体，支持指定路径并向 ripgrep 传递其他参数，映射到快捷键 \u0026lt;Leader\u0026gt;gg。\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026#34; https://github.com/junegunn/fzf.vim/issues/837#issuecomment-1179386300 \u0026#34; Search with ripgrep and fzf with folder specs. Defult starts in git root folder \u0026#34; Examples: \u0026#34; :Rg2 -w \u0026#34;apple\u0026#34; ./folder_test \u0026#34; :Rg2 -w \u0026#34;apple\u0026#34; -g \u0026#34;!themes/*\u0026#34; -g \u0026#34;*.css\u0026#34; \u0026#34; :Rg2 --type=js \u0026#34;apple\u0026#34; \u0026#34; :Rg2 --fixed-strings \u0026#34;apple\u0026#34; \u0026#34; :Rg2 -e -foo command! -bang -nargs=* Rg2 \\ call fzf#vim#grep( \\ \u0026#34;rg --column --line-number --no-heading --color=always --smart-case \u0026#34;.\u0026lt;q-args\u0026gt;, \\ 1, {\u0026#39;dir\u0026#39;: system(\u0026#39;git -C \u0026#39;.expand(\u0026#39;%:p:h\u0026#39;).\u0026#39; rev-parse --show-toplevel 2\u0026gt; /dev/null\u0026#39;)[:-2]}, \\ \u0026lt;bang\u0026gt;0) Ripgrep 搜索选项：\n-w：按单词搜索 -g：glob，用于包含或排除文件/目录（在模式前加 ! 表示排除） --type：按文件类型过滤。示例：--type=js 结果通过 fzf 展示，可以进行模糊搜索：\n\u0026quot;!pattern\u0026quot;：排除包含该模式的结果 \u0026quot;pattern\u0026quot;：模糊匹配，只包含含有该模式的结果 用 tab 选择结果，或用 c-a 全选，然后按 enter 打开，或用 c-q 构建 quickfix 列表。\n技巧：\n使用 \u0026lt;C-r\u0026gt;\u0026lt;C-w\u0026gt; 插入光标下的 word，或用 \u0026lt;C-r\u0026gt;\u0026lt;C-a\u0026gt; 插入 WORD。在 tmux 中，需要按两次 c-a。 在当前文件中搜索 # Fzf 提供了 :Lines（所有打开缓冲区）和 :BLines（当前缓冲区）命令进行行过滤。输入要搜索的词，按 enter 快速跳转到对应行。\n也可以从结果中构建 quickfix 列表：先用 \u0026lt;C-a\u0026gt; 全选，再用 \u0026lt;C-q\u0026gt; 构建。\n自定义快捷键：\n\u0026lt;Leader\u0026gt;l*：搜索所有打开的缓冲区 \u0026lt;Leader\u0026gt;/：搜索当前缓冲区（类似 /，但使用 fzf） \u0026lt;Leader\u0026gt;lw：在所有打开的缓冲区中搜索光标下的当前单词 CocSearch # 优点： 使用直观，结果以格式良好的缓冲区窗口展示 缺点： 未与 quickfix 列表集成，无法像 fzf 那样轻松过滤结果 ripgrep 参数（在 CocSearch 命令中用 -A 添加）：\n-g {GLOB}：包含或排除匹配 glob 的文件/目录（! 表示排除） -w --word-regexp：只显示被单词边界包围的匹配项 -x --line-regexp：只显示被行边界包围的匹配项 示例：搜索一个字符串，但忽略 keyboards/ 目录并排除 .mk 文件：\n1 :CocSearch STM32F411 ../../.. -g !keyboards/ -g !*.mk 搜索 Git 仓库 # 使用 Fugitive 的原生 Ggrep # fugitive.vim 提供了在 VIM 中使用 git grep 的集成，结果直接发送到 quickfix 列表。\n1 2 3 4 5 :Ggrep \u0026#34;ripgrep\u0026#34; HEAD~ :Ggrep \u0026#34;ripgrep\u0026#34; master :Ggrep -w \u0026#34;coc\u0026#34; HEAD~ -- \u0026#34;*.md\u0026#34; :Ggrep --cached findme -- \u0026#34;*.[ch]\u0026#34; :Ggrep --untracked findme 更多详情见下方关于 quickfix 列表的章节。\n使用 fzf 查看结果 # 注意：如果搜索的是不同分支或提交，FZF 的预览无法显示该文件，此时使用 fugitive 的 Ggrep 更有用。\n1 2 3 4 command! -bang -nargs=* GGrep \\ call fzf#vim#grep( \\ \u0026#39;git grep --line-number -- \u0026#39;.fzf#shellescape(\u0026lt;q-args\u0026gt;), \\ fzf#vim#with_preview({\u0026#39;dir\u0026#39;: systemlist(\u0026#39;git rev-parse --show-toplevel\u0026#39;)[0]}), \u0026lt;bang\u0026gt;0) 在 quickfix 列表中处理搜索结果 # 搜索结果保存在 quickfix 列表中，可用 :cw 打开，通过 ]q/[q 浏览（使用 vim-unimparied 插件映射），或打开 quickfix 列表后用 j/k 浏览。也可以使用 coc-list 通过自定义映射 \u0026lt;leader\u0026gt;lq 来浏览/搜索 quickfix 列表。\n当你对一个搜索完成后又需要做另一个搜索时，可以用 :col[der] 返回上一个 quickfix 列表，继续处理之前的结果。\n相关资源：\n:h quickfix :h cdo :h cfdo :h ccl - 关闭 quickfix 窗口 :h col[der] - 转到更旧的 quickfix 列表 :h cnew - 转到更新的 quickfix 列表 :h chi - 显示 quickfix 列表历史（最多 10 条） 快捷键映射 # \u0026lt;Leader\u0026gt;gg：在普通模式下进入 :Rg2 等待输入；在可视模式下以可视选区作为参数填入 \u0026lt;Leader\u0026gt;g{w|W}：搜索光标下的 word/WORD 结合快捷键 %%（展开为当前文件所在目录），可以轻松开始搜索。\n","date":"2021年5月9日","externalUrl":null,"permalink":"/zh/posts/search-in-vim/","section":"文章","summary":"搜索是开发中最常用也是最重要的操作之一。Vim 提供了非常高效和方便的搜索功能。 这篇笔记记录了一些常用的搜索命令、例子以及自定义的 key mapping。\n使用 ripgrep 搜索文件内容（:Rg2 或 ,gg，,gw/,gW） 使用 git-grep 搜索 git branch/commit，以及使用 fzf 显示 git grep 的结果（:Ggrep） 使用 fzf/coc list 搜索当前文件/buffers 中的行，实现快速定位/跳转 使用 quickfix lists 快速访问上述搜索的结果，以及利用 :cdo/:cfdo 等命令对结果进行批量操作 在文件中通用搜索 # Ripgrep 与 fzf # fzf.vim 提供了 :Rg 命令来调用 ripgrep 搜索当前目录。不过，我构建了一个更实用的变体，支持指定路径并向 ripgrep 传递其他参数，映射到快捷键 \u003cLeader\u003egg。\n","title":"在 Vim 中搜索","type":"posts"},{"content":" 搜索与处理多个文件 # 搜索 # 关于在文件中搜索词语/符号，请参见 search-in-vim。\n要快速按文件名查找并打开文件，我使用 fzf 和 coc-list，以及以下按键绑定。\n通用搜索\n要在_工作目录_（而非当前文件/缓冲区目录）中打开文件，使用 :Files 或 ,lc（list current，列出当前目录）。\n我还创建了快捷命令 ,lf（list file，列出文件），从当前缓冲区所在目录开始，可以在开始搜索前输入路径。\n此外，还有 :Buffers / ,b 用于列出已打开的缓冲区，方便快速跳转。当打开了很多文件时非常有用。不过，使用 quickfix 列表、标记，或 :Lines / ,l* 精确跳转到某一行往往更高效。\nGit 相关\n要打开当前 git 仓库中的文件，使用 :GFiles 或 ,gc（git content），这使用 git ls-files 的文件列表，会遵循 .gitignore。\n要只查看已修改的文件，使用 GFiles? 或 ,gs（git status）。\n在缓冲区之间跳转 # 在缓冲区间移动\n缓冲区有编号，可以用 n \u0026lt;C-^\u0026gt; 快速跳转到编号为 n 的缓冲区。要跳转到上一个缓冲区，直接使用 \u0026lt;C-^\u0026gt;。\n\u0026lt;C-o\u0026gt; / \u0026lt;C-i\u0026gt; 在跳转列表中前进/后退也可以用于在缓冲区之间移动，但精确度较低。\n如果只有少数几个相邻缓冲区，我将 \u0026lt;C-p\u0026gt; / \u0026lt;C-n\u0026gt; 映射为移动到上一个/下一个缓冲区。\n通过搜索移动\n当需要处理某些符号时，通常会先搜索并得到一个 quickfix 列表（详见 search-in-vim）。有了它，可以借助 quickfix 列表在搜索结果位置之间移动，速度更快。\n在大型缓冲区中移动 # 标记\n如果缓冲区中有几个经常访问的\u0026quot;热点\u0026quot;，使用标记可以轻松跳转。'a 跳转到行首，`a 跳转到精确位置。\n\u0026lsquo;a - \u0026lsquo;z 小写标记，仅在当前文件内有效 \u0026lsquo;A - \u0026lsquo;Z 大写标记，也称为文件标记，跨文件有效 动作\ng; / g,：跳转到上一个/下一个修改列表位置 [ 和 ] 跳转：方括号后接一个字符，在代码块中跳转 { } ( ) 括号：跳转到上一个/下一个未匹配的括号 m / M 函数：跳转到上一个/下一个函数的开头/结尾 #：#if #else #endif 宏跳转 *：C 注释块跳转 ( / )：跳转一个_句子_ Quickfix 列表\nQuickfix 列表不只用于搜索结果：make 命令也用它来列出带有位置信息的错误/警告，可以借此快速定位并处理问题。\n大纲\nGit 合并 # 缓冲区 # {count}Ctrl-^：跳转到编号为 count 的缓冲区。配合在顶部显示缓冲区编号的标签栏使用非常方便。 插入模式 # 动作\nCtrl-c：进入普通模式 Ctrl-O：临时切换到普通模式执行单个动作 C-x + C-e/C-y：向上/向下滚动窗口，不移动光标 删除\nCtrl-U：删除当前行中已输入的所有字符 C-w：向后删除一个单词 C-h：删除一个字符 插入\nC-i 或 Tab：插入一个制表符 C-r：插入寄存器的内容 C-t/C-d：插入/删除缩进。0 C-d 删除所有缩进 补全\nC-x 进入补全模式，之后的输入触发不同类型的补全：\nC-L：整行 C-F：文件名 C-N：缓冲区中的关键词 C-K：字典 C-]：标签 C-D：定义 C-O：omnifunc ","date":"2021年4月15日","externalUrl":null,"permalink":"/zh/posts/vim-tips/","section":"文章","summary":"搜索与处理多个文件 # 搜索 # 关于在文件中搜索词语/符号，请参见 search-in-vim。\n要快速按文件名查找并打开文件，我使用 fzf 和 coc-list，以及以下按键绑定。\n通用搜索\n要在_工作目录_（而非当前文件/缓冲区目录）中打开文件，使用 :Files 或 ,lc（list current，列出当前目录）。\n我还创建了快捷命令 ,lf（list file，列出文件），从当前缓冲区所在目录开始，可以在开始搜索前输入路径。\n此外，还有 :Buffers / ,b 用于列出已打开的缓冲区，方便快速跳转。当打开了很多文件时非常有用。不过，使用 quickfix 列表、标记，或 :Lines / ,l* 精确跳转到某一行往往更高效。\nGit 相关\n要打开当前 git 仓库中的文件，使用 :GFiles 或 ,gc（git content），这使用 git ls-files 的文件列表，会遵循 .gitignore。\n要只查看已修改的文件，使用 GFiles? 或 ,gs（git status）。\n","title":"Vim 技巧","type":"posts"},{"content":"","date":"2021年4月11日","externalUrl":null,"permalink":"/zh/categories/keyboard-design/","section":"Categories","summary":"","title":"Keyboard Design","type":"categories"},{"content":" 功能亮点 # 最初我想做一块分体键盘，但由于已经有了一块分体板（crkbd），做一块类似 Arteus/Reviunge 的一体板更有意义。\n设计采用激进式行列错位（与我的分体设计相同的列错位），列方向倾斜 15 度。\n其他功能：\nOLED 状态显示 RGB 底部灯 音频 编码器 STM32 设计考量 # 由于 F072 缺货，将使用 F411。\n在 QMK 中配置 STM 芯片的技巧：https://discord.com/channels/440868230475677696/440870965728116754/839978277489082370\n查找使用特定芯片的键盘的代码片段：\n1 2 3 4 5 6 7 8 9 10 % git grep \u0026#39;MCU\\s*=\\s*STM32F411\u0026#39; keyboards/ keyboards/handwired/onekey/blackpill_f411/rules.mk:MCU = STM32F411 keyboards/handwired/onekey/blackpill_f411_tinyuf2/rules.mk:MCU = STM32F411 keyboards/handwired/pill60/blackpill_f411/rules.mk:MCU = STM32F411 keyboards/handwired/riblee_f411/rules.mk:MCU = STM32F411 keyboards/matrix/m20add/rules.mk:MCU = STM32F411 keyboards/matrix/noah/rules.mk:MCU = STM32F411 keyboards/tkw/grandiceps/rules.mk:MCU = STM32F411 keyboards/tkw/stoutgat/v2/f411/rules.mk:MCU = STM32F411 keyboards/zvecr/zv48/f411/rules.mk:MCU = STM32F411 外设的 DMA 通道 # I2C、SPI、ADC、DAC 等外设使用 DMA 通道。PWM 也可以使用 DMA 以获得更好的性能，例如用于 LED 驱动器。STM32F072 有 7 个 DMA 通道（详情见参考手册）。选择外设时需要谨慎，避免 DMA 通道冲突。\nQMK 对以下功能使用 DMA：\nWS2812 LED：PWM 驱动使用 DMA+PWM 音频：DAC 驱动使用 DMA；PWM 驱动不使用 DMA OLED：使用 I2C，I2C 使用 DMA 通道 F072 DMA 详情 # F072 只有一个 DMA 控制器（DMA1），有 1 个通道，7 个流。\nI2C1：使用 DMA 流 6/7（或 2/3）（需要魔法启动代码进行配置） DAC1/2：使用 DMA 流 3 和 4 SPI1：流 2/3 SPI2：流 4/5 或 6/7 TIM3_CH1：流 4 TIM1_CH1：流 2 F411 DMA 详情 # F411 有 2 个 DMA 控制器，每个有 8 个通道，每通道 8 个流。\n我设计中 DMA 使用情况汇总：\n|\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;|\u0026mdash;\u0026mdash;-|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-|\n功能 QMK 驱动 引脚 外设 DMA 通道 OLED I2C PB6/7 I2C1 DMA1-Chan1-Stream5/6 音频 PWM PA8 TIM1_CH1 + TIM6 GPT N/A RGB LED PWM PB1 TIM3_CH4 DMA1-Chan5-Stream2 RGB（备选） SPI1 PB5 SPI1 DMA2-Chan3-Stream2/3 \u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;- \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash; \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;- 音频驱动 # 两种选择：DAC 和 PWM。DAC 会占用一个或两个 DMA 通道；PWM 不使用 DMA。\n使用 DAC 的音频 # 使用两个引脚可以提供更高的电压幅度和更大的音量。\n引脚（A4/A5）。参考 1 2 3 4 5 6 7 8 // 音频配置 #define AUDIO_PIN A4 #define AUDIO_PIN_ALT A5 #define AUDIO_PIN_ALT_AS_NEGATIVE #define A4_AUDIO #ifndef STARTUP_SONG # define STARTUP_SONG SONG(STARTUP_SOUND) #endif // STARTUP_SONG 使用 PWM 的音频 # 使用任意定时器的 PWM 输出方波，仅支持单引脚模式。 以 TIM3_CH1 为例（用 tim6 作为音频状态定时器）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //halconf.h: #define HAL_USE_PWM TRUE #define HAL_USE_PAL TRUE #define HAL_USE_GPT TRUE #include_next \u0026lt;halconf.h\u0026gt; // mcuconf.h: #include_next \u0026lt;mcuconf.h\u0026gt; #undef STM32_PWM_USE_TIM1 #define STM32_PWM_USE_TIM1 TRUE #undef STM32_GPT_USE_TIM6 #define STM32_GPT_USE_TIM6 TRUE //config.h: // 使用引脚 C6(TIM3_CH1) PWM #define AUDIO_PIN A8 #define AUDIO_PWM_PAL_MODE 1 #define AUDIO_PWM_DRIVER PWMD1 #define AUDIO_PWM_CHANNEL 1 #define AUDIO_STATE_TIMER GPTD6 OLED # I2C：使用 I2C1（引脚 B6/7）。参考\n注意：I2C 引脚需要上拉电阻（4.7kOhm）。\n魔法修复 # 对于 F072，正确使用 I2C1 需要额外配置。详情见 discord。\n示例代码 # 1 2 3 4 5 6 7 8 9 10 11 12 // 示例配置：https://github.com/qmk/qmk_firmware/blob/master/keyboards/xelus/kangaroo/config.h // 将 I2C1_SCL_PAL_MODE 和 I2C1_SDA_PAL_MODE 设为 1（引脚 B6/7） #define I2C1_TIMINGR_SCLDEL 3U #define I2C1_TIMINGR_SDADEL 1U #define I2C1_TIMINGR_SCLH 3U #define I2C1_TIMINGR_SCLL 9U void board_init(void) { SYSCFG-\u0026gt;CFGR1 |= SYSCFG_CFGR1_I2C1_DMA_RMP; SYSCFG-\u0026gt;CFGR1 \u0026amp;= ~(SYSCFG_CFGR1_SPI2_DMA_RMP); } 背光 RGB # LED：使用 WS2812S 使用 PWM 的 RGB 驱动 # 使用 PWM 和 DMA。这里使用引脚 B1 上的 TIM3_CH4 作为输出，通过上拉电阻连接到 5V（B1 是 5V 容忍引脚）。\n1 2 3 4 5 6 7 8 9 10 // rules.mk RGBLIGHT_DRIVER = WS2812 // config.h #define RGB_DI_PIN B1 #define WS2812_PWM_DRIVER PWMD3 #define WS2812_PWM_CHANNEL 4 #define WS2812_EXTERNAL_PULLUP #define WS2812_PWM_PAL_MODE 2 #define WS2812_DMA_STREAM STM32_DMA1_STREAM2 #define WS2812_DMA_CHANNEL 5 使用 SPI 的 RGB 驱动 # 使用 SPI2（B15）。参考\n1 2 3 4 #define WS2812_SPI SPID2 #define WS2812_SPI_MOSI_PAL_MODE 0 #define WS2812_SPI_MOSO_PAL_MODE 0 #define WS2812_SPI_SCK_PAL_MODE 0 元器件选择 # 使用此网站搜索 JLCPCB 零件，查看 SMD 贴片服务的可用性和价格。\n汇总：\n|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;|\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-|\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;|\n元件 型号 JLCPCB 料号 数量 备注 MCU STM32F072CBU6 C92504 1 稳压器 XC6206P332MR C5446 1 3.3V 固定输出 BJT MMBT3904 C20526 1 用于复位电路，需加电阻 二极管 1N4148 C81598 用于按键和复位按钮 保险丝 JK-MSMD050 C369167 用于 USB 总线电压保护 肖特基二极管 SS14 C2480 用于稳压器之间 蜂鸣器 KLJ-1625 C201041 SMD 压电蜂鸣器 MCU # 使用 STM32 MCU，比 Pro Micro 上的 atmega 更强大。atmega32u4 是 8 位处理器，只有 32K 存储。相比之下，ARM 芯片是 32 位的，程序存储更大（F072C8 有 64K，F072CB 有 128K，F411 有 256K/512K Flash 和 128K SRAM）。\nF4 系列没有内置 EEPROM 模拟器，需要外部 EEPROM 芯片，否则某些功能无法使用，例如 bootmagic 和板载设置（默认层等）。\n复位按钮电路 # 要复位（并进入 bootloader），需要同时触发 NRST 和 BOOT0 引脚。我使用中间那种方案，需要一个晶体管。\n保险丝 # Ferris 使用的型号缺货，在 LCSC 上找到了规格相近的 C369167。\n肖特基二极管 # 使用 JLCPCB 基础件 SS14，规格为\u0026quot;40V 1A 550mV @ 1A\u0026quot;，与 Ferris 使用的 RB060MM-30 接近。\n电阻和电容 # 全部使用 0603 封装。\nTODO # 下次设计需要改进的地方：\n给 I2C 引脚加 4.7k 上拉电阻 使用 8MHz 晶振代替 25MHz。参考 ","date":"2021年4月11日","externalUrl":null,"permalink":"/zh/posts/keyboard-pcb-design/","section":"文章","summary":"功能亮点 # 最初我想做一块分体键盘，但由于已经有了一块分体板（crkbd），做一块类似 Arteus/Reviunge 的一体板更有意义。\n设计采用激进式行列错位（与我的分体设计相同的列错位），列方向倾斜 15 度。\n其他功能：\nOLED 状态显示 RGB 底部灯 音频 编码器 STM32 设计考量 # 由于 F072 缺货，将使用 F411。\n在 QMK 中配置 STM 芯片的技巧：https://discord.com/channels/440868230475677696/440870965728116754/839978277489082370\n查找使用特定芯片的键盘的代码片段：\n1 2 3 4 5 6 7 8 9 10 % git grep 'MCU\\s*=\\s*STM32F411' keyboards/ keyboards/handwired/onekey/blackpill_f411/rules.mk:MCU = STM32F411 keyboards/handwired/onekey/blackpill_f411_tinyuf2/rules.mk:MCU = STM32F411 keyboards/handwired/pill60/blackpill_f411/rules.mk:MCU = STM32F411 keyboards/handwired/riblee_f411/rules.mk:MCU = STM32F411 keyboards/matrix/m20add/rules.mk:MCU = STM32F411 keyboards/matrix/noah/rules.mk:MCU = STM32F411 keyboards/tkw/grandiceps/rules.mk:MCU = STM32F411 keyboards/tkw/stoutgat/v2/f411/rules.mk:MCU = STM32F411 keyboards/zvecr/zv48/f411/rules.mk:MCU = STM32F411 外设的 DMA 通道 # I2C、SPI、ADC、DAC 等外设使用 DMA 通道。PWM 也可以使用 DMA 以获得更好的性能，例如用于 LED 驱动器。STM32F072 有 7 个 DMA 通道（详情见参考手册）。选择外设时需要谨慎，避免 DMA 通道冲突。\n","title":"键盘 PCB 设计","type":"posts"},{"content":"","date":"24 一月 2021","externalUrl":null,"permalink":"/books/amateurs-mind/","section":"Books","summary":"","title":"Amateur's Mind","type":"books"},{"content":"","date":"24 一月 2021","externalUrl":null,"permalink":"/categories/bookstudy/","section":"Categories","summary":"","title":"Bookstudy","type":"categories"},{"content":"","date":"24 一月 2021","externalUrl":null,"permalink":"/tags/pawn/","section":"Tags","summary":"","title":"Pawn","type":"tags"},{"content":"","date":"24 一月 2021","externalUrl":null,"permalink":"/tags/positional/","section":"Tags","summary":"","title":"Positional","type":"tags"},{"content":"","date":"17 一月 2021","externalUrl":null,"permalink":"/tags/imbalance/","section":"Tags","summary":"","title":"Imbalance","type":"tags"},{"content":"","date":"16 一月 2021","externalUrl":null,"permalink":"/books/chess-fundamentals/","section":"Books","summary":"","title":"Chess Fundamentals","type":"books"},{"content":"","date":"16 一月 2021","externalUrl":null,"permalink":"/tags/end-game/","section":"Tags","summary":"","title":"End Game","type":"tags"},{"content":"","date":"16 一月 2021","externalUrl":null,"permalink":"/tags/fundamental/","section":"Tags","summary":"","title":"Fundamental","type":"tags"},{"content":"","externalUrl":null,"permalink":"/zh/books/","section":"Books","summary":"","title":"Books","type":"books"}]