<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">个人资料</title>
            <subtitle type="text">用心若镜 安心若命</subtitle>
    <updated>2026-05-10T06:14:06+08:00</updated>
        <id>https://maifeipin.com</id>
        <link rel="alternate" type="text/html" href="https://maifeipin.com" />
        <link rel="self" type="application/atom+xml" href="https://maifeipin.com/atom.xml" />
    <rights>Copyright © 2026, 个人资料</rights>
    <generator uri="https://halo.run/" version="1.5.4">Halo</generator>
            <entry>
                <title><![CDATA[EdgeTTSPlayer 开发手记：打磨极致体验的本地听书神器]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/edgettsplayer-kai-fa-shou-ji--da-mo-ji-zhi-ti-yan-de-ben-de-ting-shu-shen-qi" />
                <id>tag:https://maifeipin.com,2026-05-10:edgettsplayer-kai-fa-shou-ji--da-mo-ji-zhi-ti-yan-de-ben-de-ting-shu-shen-qi</id>
                <published>2026-05-10T06:12:13+08:00</published>
                <updated>2026-05-10T06:14:06+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>作为一个喜欢听书的人，市面上虽然有不少阅读软件，但在桌面端往往难以找到一款轻量、免费且发音自然顺滑的本地听书工具。为此，我开发了 <strong>EdgeTTSPlayer</strong> —— 一款基于 <code>edge-tts</code> 和 <code>pygame</code> 构建的本地有声书播放器。</p><p>近期，为了对付体量庞大、排版混乱的网文 EPUB，以及彻底解放打包发布的双手，我对项目进行了一次大刀阔斧的重构。在此记录下这次迭代中踩过的坑与技术解决方案。</p><hr /><h2 id="1.-%E9%A9%AF%E6%9C%8D%E6%B7%B7%E4%B9%B1%E7%9A%84-epub%EF%BC%9A%E6%99%BA%E8%83%BD%E7%AB%A0%E8%8A%82%E6%A0%87%E9%A2%98%E6%8F%90%E5%8F%96" tabindex="-1">1. 驯服混乱的 EPUB：智能章节标题提取</h2><h3 id="%E7%97%9B%E7%82%B9" tabindex="-1">痛点</h3><p>在解析像《剑来》这样的网文 EPUB 时，我发现原有的章节解析算法完全失效，下拉列表里全变成了干瘪的“第 N 章”，甚至是一片空白。<br />深入扒开原生的 HTML 代码后，我发现了令人窒息的排版：</p><pre><code class="language-html">&lt;div class=&quot;header1&quot;&gt;&lt;h2&gt;&lt;b&gt;第&lt;/b&gt;&lt;b&gt;一&lt;/b&gt;&lt;b&gt;章&lt;/b&gt;&lt;/h2&gt;&lt;/div&gt;&lt;div class=&quot;part&quot;&gt;&lt;/div&gt;&lt;div class=&quot;header1&quot;&gt;&lt;h2&gt;&lt;b&gt;惊&lt;/b&gt;&lt;b&gt;蛰&lt;/b&gt;&lt;/h2&gt;&lt;/div&gt;&lt;div class=&quot;part&quot;&gt;&lt;p&gt;二月二，龙抬头...&lt;/p&gt;&lt;/div&gt;</code></pre><p>“第一章” 和 “惊蛰” 被硬生生拆分进了两个互相独立的 <code>&lt;h2&gt;</code> 标签，部分书源甚至连 <code>&lt;title&gt;</code> 和 <code>&lt;h&gt;</code> 标签都没有，全靠 <code>&lt;b&gt;</code> 加粗。</p><h3 id="%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88" tabindex="-1">解决方案</h3><p>我放弃了原本只匹配第一个 <code>&lt;h&gt;</code> 标签的简陋做法，重写了一套兼顾“规范排版”与“野生排版”的降维打击式提取算法：</p><ol><li><strong>多头合并</strong>：通过 <code>BeautifulSoup</code> 抓取前 3 个 <code>h1-h3</code> 标签，清洗后将它们用空格强行拼接，完美复原 <code>第一章 惊蛰</code>。</li><li><strong>正文嗅探（针对极简标题）</strong>：利用正则 <code>^第[零一二...]+[章回]$</code> 进行探测。如果只提取到了“第一章”，则继续切分正文的段落（Paragraph）。只要“第一章”下一段的字数少于 20 个字，就判定其为副标题并强行抓取过来。</li><li><strong>终极兜底</strong>：如果真的是没有任何标题的“三无”文件，程序会直接抽取前两个段落的内容，过滤掉所有换行与大段空白后，截取前 40 个字作为该章概要（例如 <code>陈平安看着远处的山峰...</code>）。</li></ol><p>经过这套组合拳，即使是排版再糟糕的书源，也能在软件的侧边栏呈现出美观连贯的目录树。</p><hr /><h2 id="2.-%E7%8A%B6%E6%80%81%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%8E%E2%80%9C%E6%97%A0%E7%BC%9D%E2%80%9D%E4%BA%A4%E4%BA%92%E4%BD%93%E9%AA%8C" tabindex="-1">2. 状态持久化与“无缝”交互体验</h2><h3 id="%E9%9A%8F%E6%92%AD%E9%9A%8F%E8%AE%B0%E4%B8%8E%E5%85%A8%E5%B1%80%E8%AE%B0%E5%BF%86" tabindex="-1">随播随记与全局记忆</h3><p>听书最怕的不是报错，而是突然断电后“找不到上次听到哪儿了”。<br />为此，我设计了一套基于 JSON 的细粒度持久化方案：</p><ul><li><strong>全局偏好</strong>：你的专属发音人、语速、音量会被独立记录。下次打开任何新书，都会优先加载这些偏好设置。这里曾踩过一个小坑：UI 下拉框里显示的是带详细介绍的字符串（如 <code>zh-CN-XiaoxiaoNeural (女)</code>），保存时一定要剥离出纯净的 <code>ShortName</code>，否则下次启动底层引擎会识别失败。</li><li><strong>随播随记</strong>：后台每播放完一个 Chunk（碎片文本），就会把 <code>chunk_index</code> 和联动的 <code>chapter_index</code> 写进缓存。我也在 UI 上新增了 <code>[💾 存进度]</code> 按钮以备不时之需。配合 <code>cache_version</code> 强刷新机制，完美解决了电子书解析缓存导致的脏数据问题。</li></ul><h3 id="%E5%AE%9E%E6%97%B6%E4%BB%8B%E5%85%A5%EF%BC%9A%E5%8A%A8%E6%80%81%E9%9F%B3%E9%87%8F%E4%B8%8E%E8%AF%AD%E9%80%9F%E6%84%9F%E7%9F%A5" tabindex="-1">实时介入：动态音量与语速感知</h3><p>由于我的播放机制是“双缓冲架构”（一边播放当前句子，一边后台异步请求 Edge-TTS 预生成下一句），调整语速和发音人曾经需要“停止-重新播放”才能生效。</p><ul><li><strong>音量直连</strong>：我将界面的音量滑块直接绑定了 <code>pygame.mixer.music.set_volume()</code>，实现了完全实时的音量升降。</li><li><strong>动态窥探</strong>：在后台线程每次准备生成下一个 Chunk 之前，我会通过 <code>getattr</code> 动态抓取当前主线程中选择的发音人和语速。这样，当你嫌主角语速太慢而拉动滑块时，当前这半句话读完，<strong>下一句话会直接无缝切换成新语速</strong>，完全不需要暂停或打断体验！</li></ul><hr /><h2 id="3.-%E8%A7%A3%E6%94%BE%E5%8F%8C%E6%89%8B%EF%BC%9Agithub-actions-%E8%B7%A8%E5%B9%B3%E5%8F%B0%E5%85%A8%E8%87%AA%E5%8A%A8%E5%8F%91%E7%89%88" tabindex="-1">3. 解放双手：GitHub Actions 跨平台全自动发版</h2><p>随着功能完善，每次更新都要自己跑一次 PyInstaller 实在太折磨了。更何况身为 Windows 用户，想给 Mac 和 Linux 朋友提供可执行文件几乎不可能。<br />因此，我搭建了基于 GitHub Actions 的 CI/CD 自动化流水线。</p><h3 id="%E6%A0%B8%E5%BF%83%E5%AE%9E%E7%8E%B0%EF%BC%9A" tabindex="-1">核心实现：</h3><pre><code class="language-yaml">on:  release:    types: [published]permissions:  contents: writejobs:  build:    runs-on: ${{ matrix.os }}    strategy:      matrix:        include:          - os: windows-latest          - os: macos-latest          - os: ubuntu-latest</code></pre><h3 id="%E8%B8%A9%E5%9D%91%E7%BB%8F%E9%AA%8C%EF%BC%9A" tabindex="-1">踩坑经验：</h3><ol><li><strong>触发机制的羁绊</strong>：最开始想用打 Tag 的方式触发，但后来发现将触发条件改为 <code>release: types: [published]</code> 最符合直觉。我们在 GitHub 网页端点击 <code>Draft a new release</code> 并发布的一瞬间，动作即被拉起，最后利用 <code>softprops/action-gh-release@v2</code> 就可以将编译产物自动挂载到刚创建的那个 Release 下，不需要自己去写复杂的 Release 创建脚本。</li><li><strong>环境权限陷阱</strong>：GitHub 近期收紧了 Actions 默认权限。务必记得在 Workflow 顶部加上 <code>permissions: contents: write</code>，否则编译完的 <code>.exe</code> 根本没有权限上传到 Releases 页面里（会报 <code>HttpError: Resource not accessible by integration</code> 的错）。</li><li><strong>跨平台依赖</strong>：在 Linux (Ubuntu) 虚拟服务器上跑 Tkinter 是缺少图形环境的，必须在脚本里手动执行 <code>sudo apt-get install -y python3-tk</code> 补充依赖。此外，MacOS 编译出的并非单文件，而是 <code>xxx.app</code> 目录，需要单独针对 Mac 编写 <code>zip -r</code> 压缩指令。</li></ol><p>现在，我只需要在 GitHub 随便点两下发布一个版本，系统就会拉起三台机器，几分钟后自动把 <strong>Windows版、macOS版、Linux版</strong> 打包压缩好呈现在眼前。这就是自动化的魅力！</p><hr /><p><strong>开源地址</strong>：<a href="https://github.com/maifeipin/EdgeTTSPlayer" target="_blank">maifeipin/EdgeTTSPlayer</a><br /><img src="/upload/2026/05/image.png" alt="image" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[赋能高考志愿：基于 Next.js 与内网穿透的高性能志愿决策系统实战]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/fu-neng-gao-kao-zhi-yuan--ji-yu-nextjs-yu-nei-wang-chuan-tou-de-gao-xing-neng-zhi-yuan-jue-ce-xi-tong-shi-zhan" />
                <id>tag:https://maifeipin.com,2026-04-25:fu-neng-gao-kao-zhi-yuan--ji-yu-nextjs-yu-nei-wang-chuan-tou-de-gao-xing-neng-zhi-yuan-jue-ce-xi-tong-shi-zhan</id>
                <published>2026-04-25T16:54:11+08:00</published>
                <updated>2026-04-25T17:04:37+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>高考志愿填报是人生的关键转折点。为了帮助考生更科学地从海量院校中挖掘最优选择，我开发了这套**“志愿决策系统” (Volunteer Finder)**。本文将从技术实现到实际应用，带你深度剖析这套系统的核心亮点。</p><hr /><h2 id="%F0%9F%9A%80-%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84" tabindex="-1">🚀 核心技术架构</h2><p>本系统采用现代 Web 开发的全栈方案，追求极速响应与极致视觉体验。</p><ul><li><strong>前端框架</strong>: <a href="https://nextjs.org/" target="_blank">Next.js</a> (App Router) + TypeScript</li><li><strong>样式引擎</strong>: 纯原生 CSS (Glassmorphism 玻璃拟态设计)</li><li><strong>数据库</strong>: <a href="https://sqlite.org/" target="_blank">SQLite</a> (通过 <code>better-sqlite3</code> 实现高性能查询)</li><li><strong>图标系统</strong>: <a href="https://lucide.dev/" target="_blank">Lucide React</a></li><li><strong>部署方案</strong>: 本地 Mac Mini (生产环境) + Tailscale 内网穿透 + 腾讯云 VPS (Nginx 反向代理)</li></ul><hr /><h2 id="%F0%9F%92%A1-%E6%8A%80%E6%9C%AF%E4%BA%AE%E7%82%B9%EF%BC%9A%E4%B8%8D%E4%BB%85%E6%98%AF%E6%9F%A5%E8%AF%A2%EF%BC%8C%E6%9B%B4%E6%98%AF%E6%99%BA%E8%83%BD%E6%8E%A8%E8%8D%90" tabindex="-1">💡 技术亮点：不仅是查询，更是智能推荐</h2><h3 id="1.-%E7%B2%BE%E5%87%86%E7%9A%84%E2%80%9C%E5%88%86%E6%A1%A3%E2%80%9D%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95" tabindex="-1">1. 精准的“分档”推荐算法</h3><p>系统不仅仅展示数据，更通过<strong>位次换算模型</strong>将考生的原始分数转换为全省位次，并结合近三年的投档线波动，将院校划分为：</p><ul><li><strong>冲档 (Reach)</strong>: 历史位次略高于考生，值得一试。</li><li><strong>稳档 (Match)</strong>: 位次相近，录取概率大。</li><li><strong>保底 (Safety)</strong>: 位次优势明显，确保不滑档。</li></ul><h3 id="2.-sqlite-%E6%9E%81%E9%80%9F%E9%A9%B1%E5%8A%A8" tabindex="-1">2. SQLite 极速驱动</h3><p>相比传统的云端数据库，系统直接挂载了一个高度优化的 <code>gaokao.db</code>。通过 SQLite 的索引优化，即便在处理数十万条录取数据时，查询响应时间也控制在 <strong>20ms</strong> 以内。</p><h3 id="3.-%E5%A4%9A%E7%BB%B4%E5%BA%A6%E7%AD%9B%E9%80%89%EF%BC%9A%E8%87%AA%E7%94%B1%E5%AE%9A%E4%B9%89%E4%BD%A0%E7%9A%84%E6%9C%AA%E6%9D%A5" tabindex="-1">3. 多维度筛选：自由定义你的未来</h3><p>支持<strong>多省份意向筛选</strong>。你可以通过交互式的多选标签，同时对比北京、上海、江苏等多个地区的院校分布。</p><hr /><h2 id="%F0%9F%8C%90-%E6%9E%81%E8%87%B4%E9%83%A8%E7%BD%B2%EF%BC%9A%E5%A6%82%E4%BD%95%E8%AE%A9%E5%AE%B6%E9%87%8C%E7%9A%84-mac-mini-%E5%8F%98%E6%88%90%E9%AB%98%E6%80%A7%E8%83%BD%E6%9C%8D%E5%8A%A1%E5%99%A8" tabindex="-1">🌐 极致部署：如何让家里的 Mac Mini 变成高性能服务器</h2><p>为了保证数据安全并利用本地的高性能 CPU，我采用了<strong>内网穿透 + 远程代理</strong>的部署方式：</p><ol><li><strong>本地运行</strong>: 在 Mac Mini 上运行 <code>npm run build</code> 生成生产版本，通过 <code>npm run start</code> 持续挂载。</li><li><strong>建立链路</strong>: 使用 <strong>Tailscale</strong> 建立加密通道，将本地服务暴露给 VPS。</li><li><strong>全球发布</strong>: VPS 上的 Nginx 接收 443 端口请求，通过私有链路透传回本地机器。</li></ol><p>这种方案既省去了昂贵的云服务器配置成本，又获得了物理机的超高性能。</p><hr /><h2 id="%E2%9C%A8-%E8%A7%86%E8%A7%89%E7%BE%8E%E5%AD%A6%EF%BC%9A%E9%80%8F%E6%98%8E%E6%84%9F%E4%B8%8E%E7%8E%B0%E4%BB%A3%E6%84%9F%E7%9A%84%E7%BB%93%E5%90%88" tabindex="-1">✨ 视觉美学：透明感与现代感的结合</h2><p>UI 设计采用了流行的 <strong>Glassmorphism (玻璃拟态)</strong> 风格。半透明的磨砂背景配合动态渐变边框，让复杂的表格数据也能呈现出呼吸感。</p><hr /><h2 id="%F0%9F%8E%AF-%E7%AB%8B%E5%8D%B3%E4%BD%93%E9%AA%8C" tabindex="-1">🎯 立即体验</h2><p>项目已在 GitHub 开源：<a href="https://github.com/maifeipin/volunteer-web" target="_blank">maifeipin/volunteer-web</a></p><ol><li><p>首页<br /><img src="/upload/2026/04/image-1777107654604.png" alt="image-1777107654604" /></p></li><li><p>结果页<br /><img src="/upload/2026/04/image-1777107697538.png" alt="image-1777107697538" /></p></li><li><p>学校和专业<br /><img src="/upload/2026/04/image-1777107750488.png" alt="image-1777107750488" /></p></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[曲线救国：在 Mac (M系列) 上布署高性能 Docker 远程桌面]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/qu-xian-jiu-guo--zai-macm-xi-lie--shang-bu-shu-gao-xing-neng-docker-yuan-cheng-zhun-mian" />
                <id>tag:https://maifeipin.com,2026-04-20:qu-xian-jiu-guo--zai-macm-xi-lie--shang-bu-shu-gao-xing-neng-docker-yuan-cheng-zhun-mian</id>
                <published>2026-04-20T21:33:47+08:00</published>
                <updated>2026-04-20T21:33:47+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="0.-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89%E8%BF%99%E7%AF%87%E6%96%87%E7%AB%A0%EF%BC%9F" tabindex="-1">0. 为什么要有这篇文章？</h2><p><strong>场景与痛点</strong>:<br />很多开发者习惯在任何地方、任何设备远程访问自己的 Mac (依托 Tailscale 网络)。然而，现实很残酷：</p><ul><li><strong>Mac 连 Windows</strong>: 微软官方的 <em>Microsoft Remote Desktop</em> 体验近乎原生，丝滑无比。</li><li><strong>Windows 连 Mac</strong>: 简直是灾难。系统自带的投影或传统的 VNC 协议效率极低、画质模糊、延迟巨大，完全达不到“丝滑”的标准。</li></ul><p>为了在远程也能流畅地操作 Mac 所在网络的环境（比如打开另一个内网的 WEB 服务），我们决定在 Mac 上布署一个基于 Docker 的轻量级 Linux 桌面。通过 KasmVNC 协议，在浏览器里获得<strong>超越传统 VNC 的流畅体验</strong>。</p><hr /><h2 id="1.-%E6%A0%B8%E5%BF%83%E6%96%B9%E6%A1%88%EF%BC%9Aubuntu-webtop" tabindex="-1">1. 核心方案：Ubuntu Webtop</h2><p>我们选择了 <code>linuxserver/webtop:ubuntu-xfce</code>。</p><ul><li><strong>优点</strong>: 基于 Ubuntu 生态，软件安装简单；XFCE 桌面极轻；原生支持 ARM64 (Apple Silicon) 硬件。</li><li><strong>为什么不用 Alpine?</strong>: Alpine 虽小，但缺少 <code>glibc</code> 支持，安装 Chrome 等主流浏览器时兼容性问题极多。</li></ul><h3 id="%E6%9C%80%E7%BB%88%E6%8E%A8%E8%8D%90%E9%85%8D%E7%BD%AE-(docker-compose.yml)" tabindex="-1">最终推荐配置 (<code>docker-compose.yml</code>)</h3><pre><code class="language-yaml">services:  webtop:    image: lscr.io/linuxserver/webtop:ubuntu-xfce    container_name: webtop-final    security_opt:      - seccomp:unconfined # 必须：允许容器执行更高权限的系统调用    environment:      - PUID=1000      - PGID=1000      - TZ=Asia/Shanghai      - PASSWORD=YOUR_PASSWORD # 建议设置复杂密码，用于 Web 登录和 sudo    volumes:      - ./config:/config       # 映射桌面配置和文件    ports:      - 3005:3000              # 建议更换非标准端口，增加安全性    restart: unless-stopped</code></pre><hr /><h2 id="2.-%E6%A0%B8%E5%BF%83%E6%8C%91%E6%88%98%EF%BC%9A%E5%A6%82%E4%BD%95%E5%9C%A8%E5%AE%B9%E5%99%A8%E9%87%8C%E8%A3%85%E5%A5%BD%E6%B5%8F%E8%A7%88%E5%99%A8%EF%BC%9F" tabindex="-1">2. 核心挑战：如何在容器里装好浏览器？</h2><p>在 Ubuntu Docker 镜像中，直接 <code>apt install</code> 往往会装上 <strong>Snap 版</strong>的浏览器，而 Snap 在容器内是跑不起来的。我们需要绕道安装原生的二进制版本。</p><h3 id="%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E5%AE%89%E8%A3%85-chromium-(%E9%9D%9E-snap-%E7%89%88)" tabindex="-1">第一步：安装 Chromium (非 Snap 版)</h3><p>进入容器终端（或是通过 <code>docker exec</code>）：</p><pre><code class="language-bash">sudo apt update# 添加第三方 PPA 源（提供真正的二进制版 Chromium）sudo apt install -y chromium fonts-wqy-zenhei</code></pre><h3 id="%E7%AC%AC%E4%BA%8C%E6%AD%A5%EF%BC%9A%E7%A1%AC%E8%BF%9E%E6%8E%A5%E4%BF%AE%E5%A4%8D" tabindex="-1">第二步：硬连接修复</h3><p>由于系统可能依然残留 Snap 的指向，我们需要手动强行纠正路径：</p><pre><code class="language-bash">sudo ln -sf /usr/lib/chromium/chromium /usr/bin/chromium</code></pre><p>现在，您在终端输入 <code>chromium</code> 或点击图标，就能看到秒开的浏览器了。</p><hr /><h2 id="3.-%E8%BF%9B%E9%98%B6%EF%BC%9A%E5%88%9B%E5%BB%BA%E6%A1%8C%E9%9D%A2%E4%B8%80%E9%94%AE%E5%90%AF%E5%8A%A8" tabindex="-1">3. 进阶：创建桌面一键启动</h2><p>为了像真正的 Windows 桌面一样好用，我们给 Chromium 创建一个快捷方式。</p><p>在桌面 <code>/config/Desktop/</code> 创建 <code>Chromium.desktop</code>：</p><pre><code class="language-ini">[Desktop Entry]Version=1.0Type=ApplicationName=ChromiumExec=chromium --no-sandboxIcon=chromiumTerminal=false</code></pre><p>赋予权限：<code>chmod +x ~/Desktop/Chromium.desktop</code>。</p><hr /><h2 id="4.-%E6%9E%81%E8%87%B4%E5%AE%89%E5%85%A8%E4%B8%8E%E4%BE%BF%E6%8D%B7%EF%BC%9A%E5%85%AC%E7%BD%91-ssl-%E8%AE%BF%E9%97%AE" tabindex="-1">4. 极致安全与便捷：公网 SSL 访问</h2><p>为了彻底解决浏览器的 HTTPS 强制跳转问题，建议在公网 VPS 上用 Nginx 做一层反代。</p><h3 id="%E7%94%B3%E8%AF%B7%E8%AF%81%E4%B9%A6-(%E4%BB%A5%E8%85%BE%E8%AE%AF%E4%BA%91-dns-%E9%AA%8C%E8%AF%81%E4%B8%BA%E4%BE%8B)" tabindex="-1">申请证书 (以腾讯云 DNS 验证为例)</h3><pre><code class="language-bash">certbot certonly -a dns-tencentcloud \--dns-tencentcloud-credentials /etc/letsencrypt/tencentcloud.ini \-d &quot;wt.yourdomain.com&quot; \--non-interactive --agree-tos</code></pre><h3 id="nginx-%E5%8F%8D%E4%BB%A3%E9%85%8D%E7%BD%AE-(%E5%BF%85%E9%A1%BB%E5%8C%85%E5%90%AB-websocket-%E5%8D%87%E7%BA%A7)" tabindex="-1">Nginx 反代配置 (必须包含 WebSocket 升级)</h3><pre><code class="language-nginx">server {    listen 443 ssl;    server_name wt.yourdomain.com;    ssl_certificate /etc/letsencrypt/live/wt.yourdomain.com/fullchain.pem;    ssl_certificate_key /etc/letsencrypt/live/wt.yourdomain.com/privkey.pem;    location / {        proxy_pass http://100.x.y.z:3005; # Mac 的 Tailscale IP                # 画面传输的核心：WebSocket 支持        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection &quot;upgrade&quot;;                proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_read_timeout 86400;    }}</code></pre><hr /><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>通过这套“Mac + Docker + Tailscale + VPS反代”的组合拳，我们成功解决了 Windows 远程控制 Mac 体验不佳的世纪难题。您现在拥有了一个位于 Mac 网络内部、支持丝滑网页浏览、且能从全网加密访问的高性能 Linux 桌面。</p><hr /><p><strong>Author</strong>: Antigravity AI &amp; USER<br /><strong>Date</strong>: 2026-04-20</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[多 Agent 编排全流程调试总结]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/duo-agent-bian-pai-quan-liu-cheng-diao-shi-zong-jie" />
                <id>tag:https://maifeipin.com,2026-03-29:duo-agent-bian-pai-quan-liu-cheng-diao-shi-zong-jie</id>
                <published>2026-03-29T17:47:59+08:00</published>
                <updated>2026-03-29T17:47:59+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E4%B8%80%E3%80%81%E8%83%8C%E6%99%AF" tabindex="-1">一、背景</h2><p>在此次调试开始前，系统已预置了一套基于文件队列的异步 Agent 编排骨架，目录为：</p><pre><code class="language-">~/.openclaw/workspace/memory/orchestration/├── pending/      ← main 写入待分配任务├── running/      ← specialist 认领标记├── results/      ← specialist 写入执行结果├── completed/    ← specialist 写完成标记├── watching/     ← main 监听标记└── archive/      ← 归档</code></pre><p>脚本骨架位于 <code>~/.openclaw/scripts/</code>，协议文档写在 <code>workspace/AGENTS.md</code>。</p><hr /><h2 id="%E4%BA%8C%E3%80%81phase-1-%E2%80%94-%E5%B7%A5%E4%BD%9C%E6%B5%81%E6%A1%86%E6%9E%B6%E5%AE%9A%E4%B9%89-%2B-%E5%88%9D%E6%AD%A5%E4%BB%BF%E7%9C%9F" tabindex="-1">二、Phase 1 — 工作流框架定义 + 初步仿真</h2><h3 id="%E7%94%A8%E6%88%B7%E9%9C%80%E6%B1%82" tabindex="-1">用户需求</h3><blockquote><p>“WEB main 聊天框输入：各位开始干活了。→ 各 agent 推 ‘我开始工作了’ 到手机端 → agent 把调用的工具/技能汇报给 main → main 汇总并把评语推到各自机器人客户端。”</p></blockquote><h3 id="%E5%AE%9E%E7%8E%B0%E6%AD%A5%E9%AA%A4" tabindex="-1">实现步骤</h3><ol><li><p><strong>设计事件链</strong>（Python 仿真）：</p><ul><li><code>web_input_received</code> → <code>task_assigned × N</code> → <code>push_to_user(开工)</code> → <code>task_received</code> → <code>tool_skill_called</code> → <code>task_result_reported</code> → <code>main_summary_ready</code> → <code>push_review_to_agent_client</code></li></ul></li><li><p><strong>运行仿真</strong>，产物：</p><ul><li><code>archive/2026-03-29/web-main-flow-20260329-170913/workflow-test-result.json</code></li><li>涵盖 research、mail、heartbeat 三个 specialist 的完整事件链</li></ul></li></ol><h3 id="%E4%BB%BF%E7%9C%9F%E7%BB%93%E6%9E%9C%E6%91%98%E8%A6%81" tabindex="-1">仿真结果摘要</h3><pre><code class="language-json">{  &quot;runId&quot;: &quot;web-main-flow-20260329-170913&quot;,  &quot;webInput&quot;: { &quot;message&quot;: &quot;各位开始开活了。&quot;, &quot;source&quot;: &quot;web-main-chat&quot; },  &quot;agents&quot;: [&quot;research&quot;, &quot;mail&quot;, &quot;heartbeat&quot;],  &quot;eventsCount&quot;: 18}</code></pre><hr /><h2 id="%E4%B8%89%E3%80%81phase-2-%E2%80%94-mail-%E5%B7%A5%E5%85%B7%E5%BC%82%E5%B8%B8-%2B-%E9%98%B6%E6%AE%B5%E8%AF%8A%E6%96%AD%E8%83%BD%E5%8A%9B" tabindex="-1">三、Phase 2 — Mail 工具异常 + 阶段诊断能力</h2><h3 id="%E7%94%A8%E6%88%B7%E9%9C%80%E6%B1%82-1" tabindex="-1">用户需求</h3><blockquote><p>“Mail 配置好了但不能用工具，main 应有评估工具在哪个阶段异常的能力，并在总结时反馈到各自机器人。”</p></blockquote><h3 id="%E5%AE%9E%E7%8E%B0%E6%AD%A5%E9%AA%A4-1" tabindex="-1">实现步骤</h3><h4 id="1.-%E5%88%9B%E5%BB%BA%E9%98%B6%E6%AE%B5%E8%AF%8A%E6%96%AD%E8%84%9A%E6%9C%AC" tabindex="-1">1. 创建阶段诊断脚本</h4><p><strong>文件</strong>：<code>~/.openclaw/scripts/evaluate-orchestration-stage.sh</code></p><p>核心逻辑（Python 内嵌）：</p><ul><li>读取 <code>results/{PREFIX}-*_result.json</code></li><li>关键字匹配 → 映射到阶段标签：</li></ul><table><thead><tr><th>错误关键词</th><th>阶段标签</th><th>诊断文案</th></tr></thead><tbody><tr><td><code>tool_not_available</code> / <code>command not found</code> / <code>cannot invoke tool</code></td><td><code>tool_invoke</code></td><td>工具调用阶段异常</td></tr><tr><td><code>timeout</code> / <code>imap</code></td><td><code>provider_connect</code></td><td>外部服务连接阶段异常</td></tr><tr><td>其他</td><td><code>execution</code></td><td>执行阶段异常</td></tr></tbody></table><ul><li>生成 <code>results/{PREFIX}_main_evaluation.json</code>，包含：<ul><li><code>stageFailures[]</code> — 哪个 agent 在哪个阶段失败</li><li><code>pushFeedback[]</code> — 对每个 agent 的点评消息</li></ul></li></ul><h4 id="2.-%E6%9B%B4%E6%96%B0-test-scenario-3.sh" tabindex="-1">2. 更新 <a href="http://test-scenario-3.sh" target="_blank">test-scenario-3.sh</a></h4><p>Mail 的失败模拟从 IMAP timeout 改为 <code>tool_not_available: himalaya-cli cannot invoke tool backend</code> + <code>command_not_found</code>，更真实反映工具绑定缺失场景。在 Step 2.5 插入 evaluator 调用：</p><pre><code class="language-bash">bash &quot;$HOME/.openclaw/scripts/evaluate-orchestration-stage.sh&quot; TEST-3-FAILURE-HANDLING</code></pre><blockquote><p><strong>坑</strong>：直接执行脚本报 Permission denied（未 chmod +x），改为 <code>bash &lt;path&gt;</code> 后解决。</p></blockquote><h4 id="3.-%E6%9B%B4%E6%96%B0-agents.md-%E5%8D%8F%E8%AE%AE" tabindex="-1">3. 更新 <a href="http://AGENTS.md" target="_blank">AGENTS.md</a> 协议</h4><p>在 <code>workspace/AGENTS.md</code> 的 “Result Collection &amp; Aggregation” 之后，新增 <strong>Stage Evaluation &amp; Feedback Push</strong> 节，规定 4 步：</p><ol><li>将失败结果分类到阶段标签</li><li>写入 <code>{PREFIX}_main_evaluation.json</code></li><li>为每个 agent 生成推送消息</li><li>最终汇总时附带阶段诊断</li></ol><h4 id="%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C" tabindex="-1">测试结果</h4><pre><code class="language-json">{  &quot;stageFailures&quot;: [{&quot;agent&quot;:&quot;mail&quot;,&quot;stage&quot;:&quot;tool_invoke&quot;,&quot;diagnosis&quot;:&quot;工具调用阶段异常&quot;}],  &quot;pushFeedback&quot;: [    {&quot;agent&quot;:&quot;heartbeat&quot;,&quot;status&quot;:&quot;ok&quot;},    {&quot;agent&quot;:&quot;mail&quot;,&quot;status&quot;:&quot;degraded&quot;,&quot;message&quot;:&quot;...工具调用阶段异常...请检查工具安装/配置权限...&quot;},    {&quot;agent&quot;:&quot;research&quot;,&quot;status&quot;:&quot;ok&quot;}  ],  &quot;summary&quot;: {&quot;total&quot;:3,&quot;success&quot;:2,&quot;failed&quot;:1,&quot;health&quot;:&quot;degraded&quot;}}</code></pre><hr /><h2 id="%E5%9B%9B%E3%80%81phase-3-%E2%80%94-%E5%85%A8%E6%B5%81%E7%A8%8B%E9%87%8D%E6%B5%8B%EF%BC%88%E5%90%AB%E7%94%A8%E6%88%B7%E5%8F%AF%E6%84%9F%E7%9F%A5%E7%8A%B6%E6%80%81%E6%8E%A8%E9%80%81%EF%BC%89" tabindex="-1">四、Phase 3 — 全流程重测（含用户可感知状态推送）</h2><h3 id="%E7%94%A8%E6%88%B7%E9%9C%80%E6%B1%82-2" tabindex="-1">用户需求</h3><blockquote><p>“开始对全流程编排任务重新测试。从分配工作，到接收，进行的工作，并结果反馈。通过推送让用户可感知各个 agent 的工作状态。”</p></blockquote><h3 id="%E4%BA%A7%E7%89%A9" tabindex="-1">产物</h3><p><strong><code>archive/2026-03-29/full-flow-20260329-091640/status-stream.jsonl</code></strong>：31 个事件，包含完整状态流：</p><pre><code class="language-">web_input_received  └─ task_assigned × 3      └─ push_to_user(开始执行) × 3          └─ task_received × 3              └─ tool_skill_called × 3                  └─ push_to_user(执行进度) × 3                      └─ task_result_reported × 3                          └─ main_summary_ready                              └─ push_to_user(汇总)                                  └─ push_review_to_agent_client × 3</code></pre><p><strong><code>archive/2026-03-29/full-flow-20260329-091640/final-summary.json</code></strong>：</p><pre><code class="language-json">{  &quot;totals&quot;: {&quot;assigned&quot;:3,&quot;received&quot;:3,&quot;success&quot;:2,&quot;failed&quot;:1},  &quot;mainSummary&quot;: &quot;本轮全流程编排已完成：research、heartbeat 成功；mail 在 tool_invoke 阶段异常。&quot;,  &quot;stageFailures&quot;: [{&quot;agent&quot;:&quot;mail&quot;,&quot;stage&quot;:&quot;tool_invoke&quot;}]}</code></pre><hr /><h2 id="%E4%BA%94%E3%80%81phase-4-%E2%80%94-%E7%9C%9F%E5%AE%9E%E6%B8%A0%E9%81%93%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8E%A8%E9%80%81%E6%89%93%E9%80%9A" tabindex="-1">五、Phase 4 — 真实渠道三阶段推送打通</h2><h3 id="%E7%94%A8%E6%88%B7%E9%9C%80%E6%B1%82-3" tabindex="-1">用户需求</h3><blockquote><p>“开始吧，我在WEB端输入：各位开始干活了。手机不同的IM客户端就应能推送工作状态，直到工作结束。”</p></blockquote><h3 id="%E6%8E%A8%E9%80%81%E6%B8%A0%E9%81%93%E9%85%8D%E7%BD%AE" tabindex="-1">推送渠道配置</h3><table><thead><tr><th>渠道</th><th>reply_channel</th><th>reply_to</th></tr></thead><tbody><tr><td>飞书</td><td><code>feishu</code></td><td><code>ou_7xxxxxxxxxxxx5d4</code></td></tr><tr><td>QQ 频道机器人</td><td><code>qqbot</code></td><td><code>4xxxxxxxxxxxxxxxxx7</code></td></tr><tr><td>微信</td><td><code>openclaw-weixin</code></td><td><code>o9xxxxxxxxxxxxyo@im.wechat</code></td></tr></tbody></table><h3 id="%E6%8E%A8%E9%80%81%E5%91%BD%E4%BB%A4%E6%A8%A1%E6%9D%BF" tabindex="-1">推送命令模板</h3><pre><code class="language-bash">openclaw agent --agent main \  --message &quot;&lt;内容&gt;&quot; \  --deliver \  --reply-channel &lt;channel&gt; \  --reply-to &lt;target&gt; \  --thinking off --timeout 45 --json</code></pre><h3 id="%E4%B8%89%E9%98%B6%E6%AE%B5%E6%8E%A8%E9%80%81%E7%BB%93%E6%9E%9C" tabindex="-1">三阶段推送结果</h3><table><thead><tr><th>阶段</th><th>飞书</th><th>QQBot</th><th>微信</th></tr></thead><tbody><tr><td>开工（各位开始干活了）</td><td>✅ exit=0</td><td>✅ exit=0</td><td>✅ exit=0</td></tr><tr><td>执行中（进度汇报）</td><td>✅ exit=0</td><td>✅ exit=0</td><td>✅ exit=0</td></tr><tr><td>已结束（汇总结果 JSON）</td><td>✅ exit=0</td><td>✅ exit=0</td><td>✅ exit=0</td></tr></tbody></table><hr /><h2 id="%E5%85%AD%E3%80%81%E8%B0%83%E8%AF%95%E8%BF%87%E7%A8%8B%E5%85%B3%E9%94%AE%E9%97%AE%E9%A2%98%E4%B8%8E%E8%A7%A3%E5%86%B3" tabindex="-1">六、调试过程关键问题与解决</h2><h3 id="%E9%97%AE%E9%A2%98-1%EF%BC%9A%E8%84%9A%E6%9C%AC-permission-denied" tabindex="-1">问题 1：脚本 Permission Denied</h3><p><strong>现象</strong>：<code>~/.openclaw/scripts/evaluate-orchestration-stage.sh</code> 直接调用时报 <code>Permission denied</code><br /><strong>原因</strong>：脚本未执行 <code>chmod +x</code><br /><strong>解决</strong>：调用方式改为 <code>bash &quot;$HOME/.../evaluate-orchestration-stage.sh&quot;</code> — 无需执行位</p><hr /><h3 id="%E9%97%AE%E9%A2%98-2%EF%BC%9Apython-snippet-%E6%89%B9%E9%87%8F%E8%B0%83%E7%94%A8%E8%A2%AB-cancelled" tabindex="-1">问题 2：Python snippet 批量调用被 Cancelled</h3><p><strong>现象</strong>：单个 Python 代码片段中串行发起 3 个以上 <code>subprocess.run()</code> 时，第 3 个及之后的调用被 <code>request cancelled</code><br /><strong>原因</strong>：Python runner 的执行上下文对长时间阻塞的批量调用存在取消限制<br /><strong>解决</strong>：每次只做一个渠道的 delivery 调用，拆成独立 snippet 执行</p><hr /><h3 id="%E9%97%AE%E9%A2%98-3%EF%BC%9Aqqbot-%2F-%E5%BE%AE%E4%BF%A1%22%E5%B7%B2%E7%BB%93%E6%9D%9F%22%E9%98%B6%E6%AE%B5%E8%B6%85%E6%97%B6%EF%BC%88exit%3D124%EF%BC%89" tabindex="-1">问题 3：QQBot / 微信&quot;已结束&quot;阶段超时（exit=124）</h3><p><strong>现象</strong>：<code>subprocess.run(..., timeout=25)</code> 超时退出<br /><strong>根因分析</strong>：</p><ul><li>网关日志（<code>/tmp/openclaw/openclaw-2026-03-29.log</code>）确认 <code>sessions_send</code> 已被调用</li><li><code>[qqbot-api] &gt;&gt;&gt; Body: {...}</code> 已记录，说明推送到了渠道层</li><li>超时是因为 QQBot/WeChat 的 delivery ACK 返回时间 &gt; 25s，不是路由失败</li></ul><p><strong>解决</strong>：subprocess timeout 从 25s 提高到 55s，全部成功</p><hr /><h3 id="%E9%97%AE%E9%A2%98-4%EF%BC%9A%E6%B6%88%E6%81%AF%E6%A0%BC%E5%BC%8F%E9%80%89%E6%8B%A9" tabindex="-1">问题 4：消息格式选择</h3><p><strong>背景</strong>：用户要求 QQBot、微信推送内容&quot;简单点，原样输出 JSON，只是一个提醒&quot;<br /><strong>最终格式</strong>：原始 JSON 字符串，不加 emoji 或自然语言包装：</p><pre><code class="language-json">{&quot;status&quot;:&quot;已结束&quot;,&quot;runId&quot;:&quot;live-finish-20260329-xxxxxx&quot;,&quot;summary&quot;:{&quot;research&quot;:&quot;ok&quot;,&quot;heartbeat&quot;:&quot;ok&quot;,&quot;mail&quot;:&quot;tool_invoke_error&quot;}}</code></pre><hr /><h2 id="%E4%B8%83%E3%80%81%E6%9C%80%E7%BB%88%E6%96%87%E4%BB%B6%E6%B8%85%E5%8D%95" tabindex="-1">七、最终文件清单</h2><h3 id="%E6%A0%B8%E5%BF%83%E8%84%9A%E6%9C%AC%EF%BC%88~%2F.openclaw%2Fscripts%2F%EF%BC%89" tabindex="-1">核心脚本（<code>~/.openclaw/scripts/</code>）</h3><table><thead><tr><th>文件</th><th>用途</th></tr></thead><tbody><tr><td><code>init-orchestration.sh</code></td><td>初始化 orchestration 目录结构</td></tr><tr><td><code>orchestration-test.sh</code></td><td>单次编排测试入口</td></tr><tr><td><code>run-all-tests.sh</code></td><td>运行全部场景测试</td></tr><tr><td><code>test-scenario-1.sh</code></td><td>场景1：正常全流程</td></tr><tr><td><code>test-scenario-2.sh</code></td><td>场景2：超时/部分失败</td></tr><tr><td><code>test-scenario-3.sh</code></td><td>场景3：工具不可用（mail 故障）</td></tr><tr><td><code>evaluate-orchestration-stage.sh</code></td><td>阶段异常诊断 + per-agent 推送反馈生成</td></tr></tbody></table><h3 id="%E5%8D%8F%E8%AE%AE%E6%96%87%E6%A1%A3" tabindex="-1">协议文档</h3><table><thead><tr><th>文件</th><th>关键内容</th></tr></thead><tbody><tr><td><code>workspace/AGENTS.md</code></td><td>完整 Async Agent-to-Agent 协议、任务生命周期、Stage Evaluation 节</td></tr></tbody></table><h3 id="%E8%BF%90%E8%A1%8C%E5%AD%98%E6%A1%A3%EF%BC%88workspace%2Fmemory%2Forchestration%2Farchive%2F2026-03-29%2F%EF%BC%89" tabindex="-1">运行存档（<code>workspace/memory/orchestration/archive/2026-03-29/</code>）</h3><table><thead><tr><th>目录</th><th>内容</th></tr></thead><tbody><tr><td><code>web-main-flow-20260329-170913/</code></td><td>Phase 1 工作流仿真结果</td></tr><tr><td><code>full-flow-20260329-091640/</code></td><td>Phase 3 全流程31事件状态流 + 最终汇总</td></tr><tr><td><code>live-push-20260329-172112/</code></td><td>真实开工阶段3渠道推送记录</td></tr><tr><td><code>live-flow-20260329-172245/</code></td><td>真实执行中+结束阶段6次推送记录</td></tr><tr><td><code>live-flow-finish-20260329-172505/</code></td><td>已结束阶段3渠道确认记录（最终确认版）</td></tr></tbody></table><hr /><h2 id="%E5%85%AB%E3%80%81%E4%BB%BB%E5%8A%A1%E7%8A%B6%E6%80%81%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%EF%BC%88%E6%9C%80%E7%BB%88%E7%A1%AE%E8%AE%A4%EF%BC%89" tabindex="-1">八、任务状态生命周期（最终确认）</h2><pre><code class="language-">pending/{specialist}_{taskId}.json    ↓ [specialist reads &amp; claims]running/{taskId}          ← 认领标记    ↓ [specialist executes]results/{taskId}_result.json    ↓ [specialist reports completion]completed/{taskId}        ← 完成标记    ↓ [main evaluates stages]results/{PREFIX}_main_evaluation.json    ↓ [main pushes feedback to each channel]archive/{YYYY-MM-DD}/{runId}/</code></pre><hr /><h2 id="%E4%B9%9D%E3%80%81%E5%85%B3%E9%94%AE%E7%BB%93%E8%AE%BA" tabindex="-1">九、关键结论</h2><ol><li><p><strong>文件队列协议可靠</strong>：基于文件系统的 pending→running→results→completed 状态机在所有测试场景均正确运行。</p></li><li><p><strong>阶段诊断有效</strong>：<code>evaluate-orchestration-stage.sh</code> 能准确区分 <code>tool_invoke</code> vs <code>provider_connect</code> vs <code>execution</code> 三类失败，并生成可直接推送的 per-agent 反馈。</p></li><li><p><strong>三渠道全部打通</strong>：飞书、QQBot、微信在开工/执行中/已结束三阶段均成功接收 JSON 推送（exit=0），用户手机可全程感知 agent 工作状态。</p></li><li><p><strong>推送超时根因明确</strong>：QQBot/微信 delivery ACK 慢（&gt;25s），非路由故障，subprocess timeout ≥ 55s 可稳定解决。</p></li><li><p><strong>消息格式保持原始 JSON</strong>：推送内容定位为&quot;提醒&quot;，具体执行结果取决于 agent 本地工具/技能定义，不在推送消息中展开。</p></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[OpenClaw 从单主控到 1+4 多 Agent 的实战改造复盘]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/openclaw-cong-dan-zhu-kong-dao-14-duo-agent-de-shi-zhan-gai-zao-fu-pan" />
                <id>tag:https://maifeipin.com,2026-03-29:openclaw-cong-dan-zhu-kong-dao-14-duo-agent-de-shi-zhan-gai-zao-fu-pan</id>
                <published>2026-03-29T12:01:31+08:00</published>
                <updated>2026-03-29T12:01:31+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E8%83%8C%E6%99%AF" tabindex="-1">背景</h2><p>当前 OpenClaw 长期采用单主控 Agent（main）承接几乎所有任务，问题主要有三类：</p><ol><li>职责耦合：RSS、研究、邮件、监控都压在 main，维护成本持续上升。</li><li>风险集中：某一类任务异常可能影响整条链路。</li><li>可扩展性弱：新增能力时容易改动主流程，回归成本高。</li></ol><p>本次目标是在不引入源码管理的前提下，完成一次可回滚的多 Agent 重构。</p><hr /><h2 id="%E7%9B%AE%E6%A0%87%E6%9E%B6%E6%9E%84" tabindex="-1">目标架构</h2><p>采用 1 总控 + 4 专家的中枢编排：</p><ul><li>main：总控编排、对外回复、路由与聚合</li><li>rss：RSS 抓取、归档、摘要</li><li>research：信息检索与交叉验证</li><li>mail：邮件轮询、分类与上报</li><li>heartbeat：健康巡检与告警</li></ul><h3 id="%E6%8B%93%E6%89%91%E5%9B%BE" tabindex="-1">拓扑图</h3><div class="mermaid"><svg id="render2489989653" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="302" style="max-width: 717.828125px;" viewBox="0 0 717.828125 302"><style>#render2489989653 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2489989653 .error-icon{fill:#552222;}#render2489989653 .error-text{fill:#552222;stroke:#552222;}#render2489989653 .edge-thickness-normal{stroke-width:2px;}#render2489989653 .edge-thickness-thick{stroke-width:3.5px;}#render2489989653 .edge-pattern-solid{stroke-dasharray:0;}#render2489989653 .edge-pattern-dashed{stroke-dasharray:3;}#render2489989653 .edge-pattern-dotted{stroke-dasharray:2;}#render2489989653 .marker{fill:#333333;stroke:#333333;}#render2489989653 .marker.cross{stroke:#333333;}#render2489989653 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2489989653 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2489989653 .cluster-label text{fill:#333;}#render2489989653 .cluster-label span{color:#333;}#render2489989653 .label text,#render2489989653 span{fill:#333;color:#333;}#render2489989653 .node rect,#render2489989653 .node circle,#render2489989653 .node ellipse,#render2489989653 .node polygon,#render2489989653 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2489989653 .node .label{text-align:center;}#render2489989653 .node.clickable{cursor:pointer;}#render2489989653 .arrowheadPath{fill:#333333;}#render2489989653 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2489989653 .flowchart-link{stroke:#333333;fill:none;}#render2489989653 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2489989653 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2489989653 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2489989653 .cluster text{fill:#333;}#render2489989653 .cluster span{color:#333;}#render2489989653 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2489989653 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g transform="translate(0, 0)"><marker id="flowchart-pointEnd" class="marker flowchart" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="flowchart-pointStart" class="marker flowchart" viewBox="0 0 10 10" refX="0" refY="5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker><marker id="flowchart-circleEnd" class="marker flowchart" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="flowchart-circleStart" class="marker flowchart" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></circle></marker><marker id="flowchart-crossEnd" class="marker cross flowchart" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><marker id="flowchart-crossStart" class="marker cross flowchart" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"></path></marker><g class="root"><g class="clusters"></g><g class="edgePaths"><path d="M308.453125,151L312.6197916666667,151C316.7864583333333,151,325.1197916666667,151,333.453125,151C341.7864583333333,151,350.1197916666667,151,354.2864583333333,151L358.453125,151" id="L-CH-MAIN-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-CH LE-MAIN" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M442.3583984375,134L456.4757486979167,114.16666666666667C470.5930989583333,94.33333333333333,498.8277994791667,54.666666666666664,521.41650390625,35.64880549497381C544.0052083333334,16.63094432328094,560.9479166666666,18.261888646561882,569.4192708333334,19.077360808202354L577.890625,19.89283296984282" id="L-MAIN-RSS-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-MAIN LE-RSS" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M452.49672719594594,134L464.9243559966216,124.5C477.3519847972973,115,502.2072423986486,96,522.7507159258868,89C543.294189453125,82,559.52587890625,87,567.6417236328125,89.5L575.757568359375,92" id="L-MAIN-RES-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-MAIN LE-RES" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M502.0625,158.41748042934387L506.2291666666667,158.84790035778656C510.3958333333333,159.27832028622925,518.7291666666666,160.13916014311462,531.0116780598959,163.0695800715573C543.294189453125,166,559.52587890625,171,567.6417236328125,173.5L575.757568359375,176" id="L-MAIN-MAIL-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-MAIN LE-MAIL" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M447.7650432180851,168L460.98128601507096,180.83333333333334C474.19752881205676,193.66666666666666,500.63001440602835,219.33333333333334,521.9621019295768,234.66666666666666C543.294189453125,250,559.52587890625,255,567.6417236328125,257.5L575.757568359375,260" id="L-MAIN-HB-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-MAIN LE-HB" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M577.890625,41.34293449650297L569.4192708333334,43.95244541375248C560.9479166666666,46.56195633100199,544.0052083333334,51.78097816550099,522.3176113696809,67.22382241608382C500.63001440602835,82.66666666666667,474.19752881205676,108.33333333333333,460.98128601507096,121.16666666666667L447.7650432180851,134" id="L-RSS-MAIN-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-RSS LE-MAIN" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M575.757568359375,126L567.6417236328125,128.5C559.52587890625,131,543.294189453125,136,531.0116780598959,138.9304199284427C518.7291666666666,141.86083985688538,510.3958333333333,142.72167971377075,506.2291666666667,143.15209964221344L502.0625,143.58251957065613" id="L-RES-MAIN-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-RES LE-MAIN" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M575.757568359375,210L567.6417236328125,212.5C559.52587890625,215,543.294189453125,220,522.7507159258868,213C502.2072423986486,206,477.3519847972973,187,464.9243559966216,177.5L452.49672719594594,168" id="L-MAIL-MAIN-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-MAIL LE-MAIN" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path><path d="M552.0625,284.5934421298037L547.8958333333334,284.9945351081697C543.7291666666666,285.3956280865358,535.3958333333334,286.1978140432679,517.11181640625,266.7655736883006C498.8277994791667,247.33333333333334,470.5930989583333,207.66666666666666,456.4757486979167,187.83333333333334L442.3583984375,168" id="L-HB-MAIN-0" class=" edge-thickness-normal edge-pattern-solid flowchart-link LS-HB LE-MAIN" style="fill:none;" marker-end="url(#flowchart-pointEnd)"></path></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g><g class="edgeLabel"><g class="label" transform="translate(0, 0)"><rect rx="0" ry="0" width="0" height="0"></rect><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row"></tspan></text></g></g></g><g class="nodes"><g class="node default default" id="flowchart-CH-18" transform="translate(158.2265625, 151)"><rect class="basic label-container" style="" rx="0" ry="0" x="-150.2265625" y="-17" width="300.453125" height="34"></rect><g class="label" style="" transform="translate(-142.7265625, -9.5)"><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row">Channels: Telegram/Feishu/QQ/WeCom</tspan></text></g></g><g class="node default default" id="flowchart-MAIN-19" transform="translate(430.2578125, 151)"><rect class="basic label-container" style="" rx="0" ry="0" x="-71.8046875" y="-17" width="143.609375" height="34"></rect><g class="label" style="" transform="translate(-64.3046875, -9.5)"><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row">main orchestrator</tspan></text></g></g><g class="node default default" id="flowchart-RSS-21" transform="translate(630.9453125, 25)"><rect class="basic label-container" style="" rx="0" ry="0" x="-53.0546875" y="-17" width="106.109375" height="34"></rect><g class="label" style="" transform="translate(-45.5546875, -9.5)"><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row">rss specialist</tspan></text></g></g><g class="node default default" id="flowchart-RES-23" transform="translate(630.9453125, 109)"><rect class="basic label-container" style="" rx="0" ry="0" x="-74.1875" y="-17" width="148.375" height="34"></rect><g class="label" style="" transform="translate(-66.6875, -9.5)"><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row">research specialist</tspan></text></g></g><g class="node default default" id="flowchart-MAIL-25" transform="translate(630.9453125, 193)"><rect class="basic label-container" style="" rx="0" ry="0" x="-58.953125" y="-17" width="117.90625" height="34"></rect><g class="label" style="" transform="translate(-51.453125, -9.5)"><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row">mail specialist</tspan></text></g></g><g class="node default default" id="flowchart-HB-27" transform="translate(630.9453125, 277)"><rect class="basic label-container" style="" rx="0" ry="0" x="-78.8828125" y="-17" width="157.765625" height="34"></rect><g class="label" style="" transform="translate(-71.3828125, -9.5)"><text style=""><tspan xml:space="preserve" dy="1em" x="0" class="row">heartbeat specialist</tspan></text></g></g></g></g></g></svg></div><p>设计原则：外部入口统一，内部按职责分治。</p><hr /><h2 id="%E5%AE%9E%E6%96%BD%E6%91%98%E8%A6%81" tabindex="-1">实施摘要</h2><h3 id="phase-0%EF%BC%9A%E4%B8%89%E5%B1%82%E5%A4%87%E4%BB%BD%EF%BC%88%E5%BC%BA%E5%88%B6%EF%BC%89" tabindex="-1">Phase 0：三层备份（强制）</h3><p>在改动前做了三层备份，并做了解压演练：</p><ol><li>配置层：openclaw.json、cron/jobs.json、exec-approvals.json</li><li>工作空间层：workspace、workspace-rssmanager、agents/*/agent</li><li>运行态层：sessions、cron/runs、logs</li></ol><p>结论：备份可用，支持一键回滚。</p><h3 id="phase-a%EF%BC%9A%E5%86%BB%E7%BB%93%E4%B8%8E%E5%9F%BA%E7%BA%BF%E9%87%87%E9%9B%86" tabindex="-1">Phase A：冻结与基线采集</h3><ul><li>记录改造前 agent 清单</li><li>记录现有 jobs 归属</li><li>确认旧网关状态</li></ul><h3 id="phase-c%EF%BC%9A%E5%88%9B%E5%BB%BA%E6%96%B0-agent-%E4%B8%8E-core-files-%E6%A0%87%E5%87%86%E5%8C%96" tabindex="-1">Phase C：创建新 Agent 与 Core Files 标准化</h3><p>新增 Agent：research、mail、heartbeat。<br />关键动作：统一补齐每个 Agent 的 7 个 Core Files。</p><p>Core Files 标准：</p><ol><li><a href="http://SOUL.md" target="_blank">SOUL.md</a></li><li><a href="http://IDENTITY.md" target="_blank">IDENTITY.md</a></li><li><a href="http://USER.md" target="_blank">USER.md</a></li><li><a href="http://AGENTS.md" target="_blank">AGENTS.md</a></li><li><a href="http://TOOLS.md" target="_blank">TOOLS.md</a></li><li><a href="http://MEMORY.md" target="_blank">MEMORY.md</a></li><li><a href="http://HEARTBEAT.md" target="_blank">HEARTBEAT.md</a></li></ol><p>同时保证每个 workspace 都有 memory/YYYY-MM-DD.md。</p><blockquote><p>实际经验：OpenClaw Web 控制台对 Core Files 的可见性很强，文件是否齐全直接影响后续运维体验。</p></blockquote><h3 id="phase-d%EF%BC%9A%E6%A8%A1%E5%9E%8B%E5%88%86%E5%B1%82" tabindex="-1">Phase D：模型分层</h3><p>最终落地策略采用“可用优先”，避免上线失败：</p><ul><li>main -&gt; 当前稳定主模型</li><li>rss -&gt; gemini-3.1-pro-preview</li><li>research -&gt; gpt-5-mini</li><li>mail -&gt; gpt-5-mini</li><li>heartbeat -&gt; qwen-plus</li></ul><p>说明：原计划中的部分模型在当前环境不可确认可用，因此优先使用可验证模型。</p><h3 id="phase-e%EF%BC%9A%E6%9D%83%E9%99%90%E5%88%86%E5%B1%82" tabindex="-1">Phase E：权限分层</h3><p>对 exec-approvals.json 做了最小权限划分：</p><ul><li>main 拥有 channels.send、cron.trigger、agents.invoke</li><li>specialists 默认不直接对外发送</li><li>heartbeat 允许有限通道告警</li></ul><h3 id="phase-f%EF%BC%9A%E4%BB%BB%E5%8A%A1%E9%87%8D%E6%9E%84" tabindex="-1">Phase F：任务重构</h3><ul><li>原 4 条 RSS 任务从 main 迁移到 rss</li><li>新增两条任务模板：<ul><li>Heartbeat Health Check (every 5m)</li><li>Mail Inbox Poll (every 15m)</li></ul></li></ul><p>上线策略：默认禁用新增任务，避免直接引入未知风险。</p><h3 id="phase-g%EF%BC%9A%E4%B8%8A%E7%BA%BF%E9%AA%8C%E8%AF%81" tabindex="-1">Phase G：上线验证</h3><p>完成以下检查：</p><ol><li>gateway status：RPC probe ok</li><li>agents list：5 个 Agent 全部可见</li><li>channels status --probe：主要通道可用</li><li>cron 配置：任务归属符合预期</li><li>doctor --repair：修复服务入口与 PATH 风险</li></ol><hr /><h2 id="%E5%85%B3%E9%94%AE%E7%BB%93%E6%9E%9C" tabindex="-1">关键结果</h2><h3 id="%E5%B7%B2%E8%BE%BE%E6%88%90" tabindex="-1">已达成</h3><ul><li>完成从 2 Agent 到 5 Agent 的结构升级</li><li>建立了可复用的 Core Files 初始化规范</li><li>建立了职责分明的 cron 归属策略</li><li>网关服务改为更稳定的启动入口</li><li>具备可回滚能力与回归验证清单</li></ul><h3 id="%E5%BD%93%E5%89%8D%E4%BF%9D%E5%AE%88%E4%B8%8A%E7%BA%BF%E7%8A%B6%E6%80%81" tabindex="-1">当前保守上线状态</h3><ul><li>Heartbeat 任务：已启用</li><li>Mail 任务：保持禁用（观察后再启）</li></ul><hr /><h2 id="%E8%B8%A9%E5%9D%91%E4%B8%8E%E7%BB%8F%E9%AA%8C" tabindex="-1">踩坑与经验</h2><h3 id="1)-cli-%E6%96%87%E6%A1%A3%E4%B8%8E%E5%AE%9E%E9%99%85%E8%A1%8C%E4%B8%BA%E4%B8%8D%E6%80%BB%E6%98%AF%E4%B8%80%E8%87%B4" tabindex="-1">1) CLI 文档与实际行为不总是一致</h3><p>最初按“半交互”设计，后续验证发现 add 命令可通过参数非交互执行。<br />建议：优先实测命令，再写 runbook。</p><h3 id="2)-doctor-%E4%BF%AE%E5%A4%8D%E5%90%8E%E5%BF%85%E9%A1%BB%E5%9B%9E%E5%BD%92%E9%AA%8C%E8%AF%81" tabindex="-1">2) Doctor 修复后必须回归验证</h3><p>doctor --repair 可能改写配置。<br />建议：每次修复后执行四项回归：agents、approvals、cron、gateway。</p><h3 id="3)-%E6%96%B0%E4%BB%BB%E5%8A%A1%E5%85%88%E7%A6%81%E7%94%A8%E5%86%8D%E7%81%B0%E5%BA%A6" tabindex="-1">3) 新任务先禁用再灰度</h3><p>直接启用多条新任务会增加故障定位难度。<br />建议：先启 heartbeat，观察稳定后再启 mail。</p><h3 id="4)-core-files-%E6%98%AF%E5%A4%9A-agent-%E7%9A%84%E2%80%9C%E5%8F%AF%E8%BF%90%E7%BB%B4%E5%9F%BA%E7%BA%BF%E2%80%9D" tabindex="-1">4) Core Files 是多 Agent 的“可运维基线”</h3><p>不仅是文档，更是行为边界和协作契约。</p><hr /><h2 id="%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E4%B8%8B%E6%AC%A1%E5%AE%9E%E6%96%BD%E6%A8%A1%E6%9D%BF" tabindex="-1">可复用的下次实施模板</h2><h3 id="30-%E5%88%86%E9%92%9F%E7%89%88%E6%9C%AC%EF%BC%88%E5%BB%BA%E8%AE%AE%EF%BC%89" tabindex="-1">30 分钟版本（建议）</h3><ol><li>备份 + 演练恢复（10 分钟）</li><li>新增 Agent + Core Files 初始化（8 分钟）</li><li>模型/权限/任务落地（8 分钟）</li><li>网关重启与验证（4 分钟）</li></ol><h3 id="%E6%9E%81%E7%AE%80%E6%A3%80%E6%9F%A5%E6%B8%85%E5%8D%95" tabindex="-1">极简检查清单</h3><ul class="contains-task-list"><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 备份文件可解压</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> agents list 包含目标 Agent</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> 每个 workspace 有 7 个 Core Files</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> cron 归属按职责拆分</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> gateway RPC probe ok</li><li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"> channels probe 正常</li></ul><hr /><h2 id="%E9%A3%8E%E9%99%A9%E4%B8%8E%E5%9B%9E%E6%BB%9A%E5%BB%BA%E8%AE%AE" tabindex="-1">风险与回滚建议</h2><h3 id="%E5%BB%BA%E8%AE%AE%E4%BF%9D%E7%95%99" tabindex="-1">建议保留</h3><ul><li>最近一次全量备份目录</li><li>openclaw.json.pre-model</li><li>exec-approvals.json.pre-perms</li><li>cron/jobs.json.pre-reorg</li></ul><h3 id="%E5%BB%BA%E8%AE%AE%E5%9B%9E%E6%BB%9A%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6" tabindex="-1">建议回滚触发条件</h3><p>任一条件满足即回滚：</p><ol><li>gateway 无法稳定启动</li><li>channels 探测连续失败</li><li>cron 触发异常导致主链路不可用</li><li>Agent 路由出现跨职责误调用</li></ol><hr /><h2 id="%E5%90%8E%E7%BB%AD%E4%BC%98%E5%8C%96%E8%B7%AF%E7%BA%BF" tabindex="-1">后续优化路线</h2><ol><li>清理插件冲突：处理 openclaw-lark 与 feishu 重复注册告警</li><li>为 main 增加更细粒度的路由策略（按任务类型而非仅按渠道）</li><li>增加多阶段告警等级（info/warn/critical）</li><li>给 mail 任务补充白名单与去重策略</li><li>将 runbook 固化为自动化脚本 + CI 检查项</li></ol><hr /><h2 id="%E7%BB%93%E8%AF%AD" tabindex="-1">结语</h2><p>这次改造的核心不是“加了几个 Agent”，而是把系统从“单点聪明”升级为“结构可靠”。</p><p>在无源码管理环境里，最重要的是：</p><ul><li>先可回滚，再做变更</li><li>先灰度，再全量</li><li>先验证，再宣布完成</li></ul><p>只要这三条守住，多 Agent 架构会越跑越稳。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[给xbox 装上 retroarch，用手柄打街机游戏]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/gei-xbox-zhuang-shang-retroarch-yong-shou-bing-da-jie-ji-you-xi" />
                <id>tag:https://maifeipin.com,2026-03-21:gei-xbox-zhuang-shang-retroarch-yong-shou-bing-da-jie-ji-you-xi</id>
                <published>2026-03-21T21:10:45+08:00</published>
                <updated>2026-03-21T21:42:56+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<ol><li>注册开发者账号，<a href="https://storedeveloper.microsoft.com/zh-Hans/onboarding" target="_blank">微软官方入口</a><br /><img src="/upload/2026/03/image-1774096505638.png" alt="image-1774096505638" /></li><li>在xbox  上安装 Dev Mode Activation，重启到开发者控制台模式<br /><img src="/upload/2026/03/image-1774098924894.png" alt="image-1774098924894" /></li><li>在控制台 登录开发者账号，把激活码，发送到 官网WEB网站。<br />. <img src="/upload/2026/03/image-1774096981058.png" alt="image-1774096981058" /></li><li><a href="https://github.com/libretro/RetroArch" target="_blank">下载 RetroArch 源代码</a> 并编译<br /><img src="/upload/2026/03/image-1774097596273.png" alt="image-1774097596273" /></li><li>把编译后的 RetroArch-msvcUWP_1.22.2.0_x64_ReleaseANGLE.appxbundle 拖到 dev home的http服务的页面（添加应用和游戏按钮后弹出）<br /><img src="/upload/2026/03/image-1774097969473.png" alt="image-1774097969473" /></li><li>在回到xbox 的 dev home中 运行 retroarch，然后 在updater online 下载核心和配置.<br /><img src="/upload/2026/03/image-1774098236293.png" alt="image-1774098236293" /></li><li>最后，如果不想自己动手，在某宝某鱼上搜 retroarch 也可以代装。本人不打游戏，但看司波图的这玩意介绍视频后发现这个可以替换家里的N1盒子了，N1盒子的遥控器太烂了。有YT,ATV还有KODI,完美平替，找个信用好的卖家xbox one s 1T版本， 价格聊到500以内果断入手。</li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[阿里小龙虾，内测版]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/a-li-xiao-long-xia--nei-ce-ban" />
                <id>tag:https://maifeipin.com,2026-03-15:a-li-xiao-long-xia--nei-ce-ban</id>
                <published>2026-03-15T13:47:03+08:00</published>
                <updated>2026-03-15T13:48:06+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="%E7%AE%80%E5%8D%95%E7%94%B3%E8%AF%B7" tabindex="-1">简单申请</h3><ol><li><a href="https://jvs.wuying.aliyun.com/login" target="_blank">申请免费内测链接</a><br />非常简单<br /><img src="/upload/2026/03/image-1773553253725.png" alt="image-1773553253725" /><br />很快收到邮件<br /><img src="/upload/2026/03/image-1773553312643.png" alt="image-1773553312643" /></li></ol><h3 id="%E9%85%8D%E7%BD%AE%E5%BE%88%E5%A4%A7%E6%96%B9" tabindex="-1">配置很大方</h3><ol><li><p>完全预装 OPENCLAW+ AI模型<br /><img src="/upload/2026/03/image-1773553370846.png" alt="image-1773553370846" /></p></li><li><p>4C8G100G<br /><img src="/upload/2026/03/image-1773553606000.png" alt="image-1773553606000" /></p></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[给小龙虾装QQ机器人]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/gei-xiao-long-xia-zhuang-qq-ji-qi-ren" />
                <id>tag:https://maifeipin.com,2026-03-14:gei-xiao-long-xia-zhuang-qq-ji-qi-ren</id>
                <published>2026-03-14T13:18:44+08:00</published>
                <updated>2026-03-15T14:13:38+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="qq%E6%9C%BA%E5%99%A8%E4%BA%BA%E9%93%BE%E6%8E%A5" tabindex="-1">QQ机器人<a href="https://q.qq.com/qqbot/openclaw/index.html" target="_blank">链接</a></h3><p><img src="/upload/2026/03/image-1773465386884.png" alt="image-1773465386884" /></p><h3 id="%E5%BC%80%E5%A7%8B%E5%AF%B9%E8%AF%9D" tabindex="-1">开始对话</h3><p><img src="/upload/2026/03/image-1773465735024.png" alt="image-1773465735024" /></p><h3 id="%E5%B7%B2%E8%A3%85%E8%BF%87%E5%B0%8F%E9%BE%99%E8%99%BE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E9%80%9A%E8%BF%87-openclaw-config-%E5%91%BD%E4%BB%A4-%E9%85%8D%E7%BD%AE-model-%E5%92%8C-bot" tabindex="-1">已装过小龙虾，可以通过   openclaw config 命令 配置 MODEL 和 BOT</h3><h3 id="tg%E6%9C%BA%E5%99%A8%E4%BA%BA" tabindex="-1">TG机器人</h3><p><img src="/upload/2026/03/image-1773468558535.png" alt="image-1773468558535" /><br /><img src="/upload/2026/03/image-1773468637830.png" alt="image-1773468637830" /><br /><img src="/upload/2026/03/image-1773468704236.png" alt="image-1773468704236" /><br /><img src="/upload/2026/03/image-1773468745604.png" alt="image-1773468745604" /></p><h3 id="%E9%A3%9E%E4%B9%A6%E6%9C%BA%E5%99%A8%E4%BA%BA" tabindex="-1">飞书机器人</h3><p><a href="https://www.feishu.cn/content/article/7613711414611463386" target="_blank">官方链接</a></p><pre><code class="language-">npx -y @larksuite/openclaw-lark-tools installnpx -y @larksuite/openclaw-lark-tools update</code></pre><p>安装以后出现二维码， 扫描创建成功。</p><pre><code class="language-流式输出">openclaw config set channels.feishu.streaming true</code></pre><p>默认feishu 是禁用的，国际版lark开启的,需要手动修改~openclaw/openclaw.json 开启<br /><img src="/upload/2026/03/image-1773475921514.png" alt="image-1773475921514" /></p><h3 id="%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E5%AE%98%E6%96%B9%E4%B8%80%E9%94%AE%E7%9B%B4%E8%BE%BE%E7%BD%91%E5%9D%80" tabindex="-1">企业微信官方<a href="https://open.work.weixin.qq.com/help/wap/detail?docid=21657" target="_blank">一键直达网址</a></h3><p>一键扫码，创建机器</p><pre><code class="language-">npx -y @wecom/wecom-openclaw-cli install</code></pre><p><img src="/upload/2026/03/image-1773538912296.png" alt="image-1773538912296" /></p><p>聊天对话：</p><p><img src="/upload/2026/03/image-1773539164472.png" alt="image-1773539164472" /></p><h3 id="web-%E6%8E%A7%E5%88%B6%E5%8F%B0" tabindex="-1">web 控制台</h3><p><img src="/upload/2026/03/image-1773539322461.png" alt="image-1773539322461" /></p><h3 id="%E8%87%AA%E5%8A%A8%E5%A2%9E%E5%8A%A0%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%EF%BC%8C%E7%94%9F%E6%88%90%E8%87%AA%E6%88%91%E8%BF%9B%E5%8C%96%E7%9A%84%E6%8A%80%E8%83%BD" tabindex="-1">自动增加定时任务，生成自我进化的技能</h3><p><img src="/upload/2026/03/image-1773501197507.png" alt="image-1773501197507" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[拒绝小龙虾（openclaw），让AI Agent 在你眼皮底下干干净净干活。]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/拒绝小龙虾openclaw让aiagent在你眼皮底下干干净净干活" />
                <id>tag:https://maifeipin.com,2026-03-07:拒绝小龙虾openclaw让aiagent在你眼皮底下干干净净干活</id>
                <published>2026-03-07T18:42:37+08:00</published>
                <updated>2026-03-08T10:47:25+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>这里不得不提到最近爆火的 <strong>OpenClaw</strong>。作为一个能全自动接管浏览器、终端、甚至帮你处理邮件、买东西的明星 Agent，OpenClaw 的强大毋庸置疑。但“全自动”往往也意味着“全盲区”——当它在后台疯狂跳动时，你作为代码的主人，却成了最无知的旁观者。</p><p>今天，我们将展示一种全新的本地混合智能架构——<strong>MyAgent</strong>。它不仅仅是一套基于 C# (WPF) 构建的现代化原生桌面客户端，更是一套<strong>完全透明化、高度可控、基于 YAML 白盒编排</strong>的混合双打自律系统。</p><p>它将大模型的决策力与本地桌面级极客权限完美融合，并让所有的动作实况，毫无保留地投射在你的眼皮底下。</p><hr /><h2 id="%F0%9F%8C%9F-%E6%A0%B8%E5%BF%83%E7%90%86%E5%BF%B5%EF%BC%9A%E4%BB%8E%E2%80%9C%E9%BB%91%E7%9B%92%E4%BB%A3%E5%B7%A5%E2%80%9D%E8%B5%B0%E5%90%91%E2%80%9C%E7%99%BD%E7%9B%92%E7%BB%93%E5%AF%B9%E7%BC%96%E7%A8%8B%E2%80%9D" tabindex="-1">🌟 核心理念：从“黑盒代工”走向“白盒结对编程”</h2><h3 id="1.-%E7%81%B5%E9%AD%82%E8%BF%9B%E5%8C%96%EF%BC%9A%E5%85%B7%E5%A4%87%E2%80%9C%E7%B2%BE%E8%AF%BB%E4%BB%A3%E7%A0%81%E2%80%9D%E4%B8%8E%E2%80%9C%E8%87%AA%E6%88%91%E9%87%8D%E6%9E%84%E2%80%9D%E7%9A%84%E6%95%B0%E5%AD%97%E7%94%9F%E5%91%BD" tabindex="-1">1. 灵魂进化：具备“精读代码”与“自我重构”的数字生命</h3><p>不同于那些只会按部就班执行脚本的工具，MyAgent 具备深度的<strong>自我进化</strong>能力。</p><ul><li><strong>底层代码自察</strong>：通过内置的 <code>agent.update_config</code> 工具，AI 不再只是在脚本里跑，它可以<strong>精读自己的系统配置甚至能力库代码</strong>。当它发现当前的网络连接不稳定或模型参数需要优化时，它会主动提出修改方案并自我重写配置文件。</li><li><strong>动态技能注入</strong>：你可以对它说：“帮我写一个自动巡检服务器并发送飞书通知的工具”。它不仅会写脚本，还会自动生成对应的 <strong>YAML 技能剧本</strong> 并将其注入到 <code>config/skills</code> 目录中，在不重启的情况下直接解锁新技能。<br /><img src="/upload/2026/03/image-1772937868991.png" alt="image-1772937868991" /></li></ul><h3 id="2.-%E5%8F%8C%E6%A0%B8%E9%A9%B1%E5%8A%A8%E7%9A%84%E9%9B%B6%E6%AD%BB%E8%A7%92%E2%80%9C%E5%AE%9E%E5%86%B5%E6%B5%8F%E8%A7%88%E5%99%A8%E2%80%9D-(sandboxed-webview2)" tabindex="-1">2. 双核驱动的零死角“实况浏览器” (Sandboxed WebView2)</h3><p>许多 Agent 在后台跑无头流（Headless）时，用户对网页动态一无所知。MyAgent 将微软的工业级 Chromium 内核（WebView2）物理嵌入到了程序右侧。</p><ul><li><strong>所见即所得的感知控制</strong>：AI 在网页上点击哪个按钮、输入了什么字符、跳转到了哪个页面，你都能在【🌐 浏览器进程实况】面板中实时看到动态红框标记。</li><li><strong>AI 意图寻址推理</strong>：只需给出一句“帮我找一下最近的 AI 行业新闻”，系统会自动调用 <code>perception.dom</code> 提取 DOM 树，配合 LLM 实时决策下一步是滚动页面还是点击跳转，而这一切“思考”过程都透明地展现在你的 UI 交互日志中</li><li><img src="/upload/2026/03/image-1772937786450.png" alt="image-1772937786450" /></li></ul><h3 id="3.-%E5%8F%AF%E8%A7%86%E5%8C%96%E8%BF%90%E7%BB%B4%E4%B8%93%E5%AE%B6%EF%BC%9Axterm.js-%2B-ai-%E7%9A%84%E7%AB%AF%E5%88%B0%E7%AB%AF%E5%8D%8F%E5%90%8C" tabindex="-1">3. 可视化运维专家：xterm.js + AI 的端到端协同</h3><p>云端运维是一个极其危险的高敏场景。MyAgent 引入了原生的 <strong>xterm.js 终端子系统</strong>，将 AI 的运维能力与人类的监控权锁死在一起。<img src="/upload/2026/03/image-1772937224636.png" alt="image-1772937224636" /></p><ul><li><strong>全双工交互 Shell</strong>：系统通过 SSH 通道连接服务器，并为大模型提供了一个虚拟键盘接口。AI 并非一次性丢出一串脚本，而是像极客一样敲一行命令，读取一次 xterm.js 的实时回显，再根据报错信息动态修正后输入下一行。</li><li><strong>极致的安全围栏</strong>：所有的凭证仅留在本地物理内存；你在界面上看到的每一个字符闪烁，都是 AI 与真实服务器交互的物理实录。当 AI 动作过于激进时，你随时可以物理拦截，夺回主控权。</li></ul><h3 id="4.-%E6%9E%81%E8%87%B4%E4%B8%94%E8%8B%9B%E5%88%BB%E7%9A%84%E7%BD%91%E7%BB%9C%E7%A9%BF%E9%80%8F%E4%BB%A3%E7%90%86%EF%BC%88socks5-%2F-http%EF%BC%89" tabindex="-1">4. 极致且苛刻的网络穿透代理（Socks5 / HTTP）</h3><p>现代的 API 往往面临严酷的网络防火墙。</p><ul><li><strong>全链路代理</strong>：不单是给 <code>HttpClient</code> 设置代理池，我们在内核级别为 WebView2 强制注入了 <code>--proxy-server</code> 指令。</li><li><strong>Agentic Self-Configuration</strong>：如果网络不通，Agent 甚至会尝试自主修改自身的代理地址配置进行“自救”，这种打破“第四面墙”的能力，让它在复杂网络环境下拥有极强的韧性。<br /><img src="/upload/2026/03/image-1772937973114.png" alt="image-1772937973114" /></li></ul><h3 id="5.-%E9%AB%98%E6%95%88%E5%8F%8D%E9%A6%88%E6%B5%81%EF%BC%9A%E4%BB%8E-ai-%E7%BB%93%E6%99%B6%E4%BB%93%E5%88%B0%E4%BC%81%E4%B8%9A-webhook" tabindex="-1">5. 高效反馈流：从 AI 结晶仓到企业 Webhook</h3><p>Agent 干完活后不能只留下一堆生硬的 Log 控制台。</p><ul><li><strong>Markdown 智能报告</strong>：原生提供 Markdown 渲染面板，直接把枯燥的长日志蒸馏为排版美观的图文报告。</li><li><strong>企业直联</strong>：内置 <code>WebhookNotifyTool</code>，系统诊断或执行的结果可以一键推送到钉钉、飞书工作群，实现“端到云”的监控闭环。</li></ul><h2 id="%E7%BB%93%E8%AF%AD" tabindex="-1">结语</h2><p>真正的 AI 代理，应当是<strong>你手中的尖刀，而不是脱缰的野马</strong>。</p><blockquote><p>项目地址 / 开源仓库：<a href="https://github.com/maifeipin/MyAgent" target="_blank">maifeipin/MyAgent</a></p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[AI Agent 自动化系统 - 开发设计文档]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/aiagent-zi-dong-hua-xi-tong---kai-fa-she-ji-wen-dang" />
                <id>tag:https://maifeipin.com,2026-03-07:aiagent-zi-dong-hua-xi-tong---kai-fa-she-ji-wen-dang</id>
                <published>2026-03-07T15:54:57+08:00</published>
                <updated>2026-03-07T15:54:57+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="ai-agent-%E8%87%AA%E5%8A%A8%E5%8C%96%E7%B3%BB%E7%BB%9F---%E5%BC%80%E5%8F%91%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3" tabindex="-1">AI Agent 自动化系统 - 开发设计文档</h1><blockquote><p><strong>版本:</strong> v1.1<br /><strong>日期:</strong> 2026 年 3 月<br /><strong>目标:</strong> 构建一个可学习、可积累、可复用的桌面级 AI Agent 系统<br /><strong>阅读对象:</strong> 系统架构师、后端研发工程师、WPF 客服端研发工程师、AI 能力对接工程师。本记录作为编码落地的直接依据。</p></blockquote><hr /><h2 id="1.-%E7%B3%BB%E7%BB%9F%E6%A6%82%E8%BF%B0" tabindex="-1">1. 系统概述</h2><h3 id="1.1-%E6%A0%B8%E5%BF%83%E7%90%86%E5%BF%B5" tabindex="-1">1.1 核心理念</h3><p>智能体系统的核心价值在于<strong>将“人工操作路径”抽象并固化为“机器可执行流程”</strong>，并且允许机器在执行过程中根据环境变化进行<strong>感知降级</strong>与<strong>自我修正</strong>。</p><pre><code class="language-text">┌─────────────────────────────────────────────────────────────┐│ 🧠 智能体核心思想                                           │├─────────────────────────────────────────────────────────────┤│ 1. 技能不写死 → 全部配置化 (Skill Config)，实现逻辑层面解耦 ││ 2. 过程可记录 → 执行日志结构化 (Execution Log)，便于追溯异常││ 3. 经验可复用 → 成功路径可固化为标准模板 (Success Pattern)  ││ 4. 能力可进化 → 失败重试及 AI 辅助修正生成新技能 (Learning) │└─────────────────────────────────────────────────────────────┘</code></pre><h3 id="1.2-%E7%94%A8%E6%88%B7%E5%9C%BA%E6%99%AF%E4%B8%8E%E8%BE%B9%E7%95%8C" tabindex="-1">1.2 用户场景与边界</h3><table><thead><tr><th>场景</th><th>频率</th><th>复杂度</th><th>核心诉求</th><th>依赖的技能类型</th></tr></thead><tbody><tr><td>网易邮箱查账单</td><td>每月</td><td>中</td><td>定期提取多封邮件数据，结构化输出</td><td>感知 (DOM/OCR) + 执行 + 数据解析</td></tr><tr><td>央视体育直播</td><td>按需</td><td>低</td><td>自动打开特定网页并全屏播放，防休眠</td><td>执行 (Navigation/Window) + 媒体控制</td></tr><tr><td>RSS 头条阅读</td><td>每日</td><td>低</td><td>聚合多个数据源，提炼核心结论推送到端</td><td>感知 (API Fetch) + 总结生成 (AI)</td></tr><tr><td>自定义工作流</td><td>-</td><td>高</td><td>用户提供自然语言或录制，系统自动生成配置</td><td>Agent Planner + 动态生成配置</td></tr></tbody></table><h3 id="1.3-%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99" tabindex="-1">1.3 设计原则</h3><table><thead><tr><th>原则</th><th>落地准则</th></tr></thead><tbody><tr><td><strong>配置驱动</strong></td><td>代码本体不硬编码任何具体业务逻辑（如网址、DOM Selector），全由 JSON/YAML 载入。</td></tr><tr><td><strong>感知降级</strong></td><td>网页解析时，优先使用速度最快、无成本的 DOM 解析；失败时回退至本地 OCR 识别屏幕坐标；再次失败使用多模态大模型 (VL) 进行视觉理解。</td></tr><tr><td><strong>单进程宿主</strong></td><td><code>Main.exe</code> 作为唯一宿主入口。浏览器利用 WebView2 (独立子进程不崩主进程)；耗时任务通过 <code>Task.Run</code> 在后台线程池调度并派发状态。</td></tr><tr><td><strong>防破坏性</strong></td><td>系统必须提供运行状态可视化（正在做什么），且支持强制全局中断（Esc 或快捷键）。涉及资产或高危操作（如下单）必须加入人工二次确认 (Human-in-the-loop)。</td></tr></tbody></table><hr /><h2 id="2.-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E6%B7%B1%E5%85%A5" tabindex="-1">2. 系统架构深入</h2><h3 id="2.1-%E6%A8%A1%E5%9D%97%E4%BE%9D%E8%B5%96%E4%B8%8E%E6%9E%B6%E6%9E%84%E5%88%86%E5%B1%82" tabindex="-1">2.1 模块依赖与架构分层</h3><p>架构严格遵循依赖倒置原则（DIP）。上层依赖下层的接口，具体实现在 DI 容器中注入。</p><pre><code class="language-text">┌────────────────────────────────────────────────────────────────────────────────────────┐│ UI 层 (MyAgent.UI) | MVVM 架构 | WPF + CommunityToolkit.Mvvm                           ││ 负责展示面板、接管全局快捷键、与系统托盘交互。通过 EventAggregator 接收内核状态通知。  │├────────────────────────────────────────────────────────────────────────────────────────┤│ 业务编排层 (MyAgent.Host)                                                              ││ 负责装配 DI 容器，初始化数据库，拉起 Scheduler (Quartz.NET)，管理系统生命周期。        │├─────────┬──────────────────────────────────────────────────────────────────────────────┤│         │ 核心调度层 (MyAgent.Core)                                                    ││ 基础    │ 暴露 IAgentEngine, ISkillManager, ILogService 等接口。                       ││ 设施    │ 负责：1. 解析 YAML 加载 Skill 树。 2. 调度具体的 Step 运行。                 ││         │       3. 上下文透传与流转。        4. 统一处理异常与重试逻辑。               ││ Utils   ├──────────────────────────────────────────────────────────────────────────────┤│ (SQLite │ 工具技能层 (MyAgent.Skills)                                                  ││  Http   │ 1. Perception (感知): DOM 解析器, PaddleOCR 包装器, Qwen-VL 视觉请求客户端。 ││  Log)   │ 2. Action (动作): WebView2 控制器 (点击/输入), 鼠标/键盘低级模拟 (Win32)。   ││         │ 3. Media (媒体): FFmpeg 录屏调用, 音频播报等。                               ││         │ 4. AI (认知): Qwen API 客户端，封装 Prompt 模板。                            │└─────────┴──────────────────────────────────────────────────────────────────────────────┘</code></pre><h3 id="2.2-%E4%B8%8A%E4%B8%8B%E6%96%87%E6%B5%81%E8%BD%AC%E6%9C%BA%E5%88%B6-(context-flow)" tabindex="-1">2.2 上下文流转机制 (Context Flow)</h3><p>每个 Skill 的执行都会生成一个唯一的 <code>SkillExecutionContext</code>，用于在多个 Step 之间传递数据。</p><pre><code class="language-csharp">public class SkillExecutionContext{    public string ExecutionId { get; set; } = Guid.NewGuid().ToString(&quot;N&quot;);    public string SkillId { get; set; }        // 运行时环境变量 (例如：当前绑定的 WebView2 句柄, 全局超时等)    public Dictionary&lt;string, object&gt; EnvironmentArgs { get; set; }        // 步骤间数据传递 (Step 1 提取的数据，存入此处供 Step 2 的 AI 总结使用)    public Dictionary&lt;string, object&gt; StateBag { get; set; }        // 取消令牌，用于响应全局中止事件    public CancellationTokenSource CancellationToken { get; set; }        public ExecutionLogData Logger { get; set; }}</code></pre><hr /><h2 id="3.-%E8%AF%A6%E7%BB%86%E7%9A%84-skill-%E5%BC%95%E6%93%8E%E8%AE%BE%E8%AE%A1" tabindex="-1">3. 详细的 Skill 引擎设计</h2><h3 id="3.1-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%A7%A3%E6%9E%90%E4%B8%8E%E5%8F%8D%E5%B0%84%E8%B7%AF%E7%94%B1" tabindex="-1">3.1 工作流解析与反射路由</h3><p>配置文件中的 <code>action: &quot;browser.navigate&quot;</code> 是如何转换为代码执行的？</p><ol><li>系统启动时，扫描 <code>MyAgent.Skills</code> 程序集。</li><li>查找所有标记了 <code>[SkillAction(&quot;browser.navigate&quot;)]</code> 的类，这些类必须实现 <code>IActionTool</code> 接口。</li><li>将 <code>&quot;browser.navigate&quot;</code> 字符串作为 Key 注册到字典缓存中。</li><li>运行时，引擎读取 YAML 中的 <code>action</code> 字段，从字典取出现实类，利用反射（或 ActivatorUtilities）通过 DI 创建实例。</li><li>将 <code>params</code> 节点反序列化为 <code>JObject</code> 或强类型类传入 <code>ExecuteAsync</code>。</li></ol><h3 id="3.2-%E5%AE%8C%E6%95%B4%E7%9A%84-yaml-%E9%85%8D%E7%BD%AE%E8%A7%84%E8%8C%83%E5%AE%9A%E4%B9%89" tabindex="-1">3.2 完整的 YAML 配置规范定义</h3><p>为了让 AI 或人类更好编写配置，以下是标准化的 JSON Schema 概念：</p><pre><code class="language-yaml"># 规范化 skill 定义文件schema_version: &quot;1.1&quot;skill_id: &quot;example_skill_id&quot;name: &quot;技能名称&quot;description: &quot;技能的自然语言描述，供规划器 Planner 理解该技能用于什么场景&quot;trigger:  type: schedule | manual | event | api # 触发方式  cron: &quot;0 9 * * *&quot; # 当 type=schedule 必填  keywords: [&quot;关键词1&quot;] # 给意图识别模型用于匹配workflow:  - step_id: &quot;step_1&quot;    name: &quot;本步骤含义&quot;    action: &quot;命名空间.动作名&quot; # 必须是已注册的组件    params: { ... } # 对应 action 的入参，可使用 {{StateBag.Key}} 解析上文变量    timeout_ms: 5000 # 步骤级超时控制    retry_policy: # 步骤级重试策略      max_attempts: 3      delay_ms: 1000    on_error: &quot;continue&quot; | &quot;abort&quot; | &quot;fallback&quot; # 出错后的行为    fallback_action: # 当 on_error=fallback 时必填      action: &quot;...&quot;      params: { ... }success_criteria: # 决定该 Skill 是否计为最终成功的条件  - type: element_exists | data_extracted | media_playing    condition: { ... }</code></pre><h3 id="3.3-%E5%8F%98%E9%87%8F%E6%8F%92%E5%80%BC%E4%B8%8E%E5%8A%A8%E6%80%81%E5%8F%82%E6%95%B0-(%E6%96%B0%E7%89%B9%E6%80%A7)" tabindex="-1">3.3 变量插值与动态参数 (新特性)</h3><p>在复杂的流程中，后续步骤需要使用前面步骤的结果。我们需要支持类似 <code>{{}}</code> 的模板语法。<br />例如：步骤 A 获取了用户名，保存到了 <code>StateBag[&quot;username&quot;]</code>。步骤 B 调用 API 时，<code>url: &quot;https://api.com/user/{{username}}&quot;</code>。引擎在执行前会正则替换并注入实际值。</p><hr /><h2 id="4.-%E7%A8%B3%E5%81%A5%E7%9A%84%E9%87%8D%E8%AF%95%E4%B8%8E%E7%BD%91%E7%BB%9C%E4%BF%9D%E6%8A%A4" tabindex="-1">4. 稳健的重试与网络保护</h2><h3 id="4.1-%E5%BC%82%E5%B8%B8%E5%88%86%E7%B1%BB%E5%99%A8" tabindex="-1">4.1 异常分类器</h3><p>为了保证 Agent 在无人值守下的稳定性，必须对异常进行精确分类：</p><ol><li><strong>TransientException (瞬态异常):</strong> 例如网络超时、元素加载暂时不可见。<ul><li><strong>策略:</strong> 按照 <code>retry_policy</code> (指数退避算法) 进行重试。</li></ul></li><li><strong>BusinessLogicException (业务异常):</strong> 网站提示“密码错误”、“验证码错误”。<ul><li><strong>策略:</strong> 降级或直接终止（abort），因为重试多半无用。通知用户 <code>Notification.Send</code>。</li></ul></li><li><strong>PerceptionException (感知异常):</strong> DOM 结构变化导致 <code>SelectorNotFound</code>。<ul><li><strong>策略:</strong> 触发配置的 <code>perception.fallback_chain</code>（DOM -&gt; OCR -&gt; VL）。如果全部降级失败，记录为“技能失效需人工修正”。</li></ul></li><li><strong>FatalException (致命异常):</strong> 浏览器内核崩溃、内存溢出。<ul><li><strong>策略:</strong> 捕获于最外层，重启 WebView2 进程环境，清理相关分配。</li></ul></li></ol><h3 id="4.2-webdriver-%E4%B8%8E-dom-%E7%9A%84%E4%BA%A4%E4%BA%92%E9%9A%94%E7%A6%BB" tabindex="-1">4.2 WebDriver 与 DOM 的交互隔离</h3><p>使用 WebView2 的 <code>ExecuteScriptAsync</code> 执行 JS 与页面交互时，强烈建议注入一段<strong>受控的内嵌脚本库 (Agent.js)</strong>，而不是零散地写原生 JS 代码。<br /><code>Agent.js</code> 负责平滑滚动、带重试的元素查找、消除遮罩层等稳定化操作，极大降低 C# 侧交互的复杂度。</p><hr /><h2 id="5.-%E5%AD%98%E5%82%A8%E6%A8%A1%E5%9E%8B%E8%AE%BE%E8%AE%A1-(sqlite)" tabindex="-1">5. 存储模型设计 (SQLite)</h2><h3 id="5.1-%E5%AE%9E%E4%BD%93%E5%85%B3%E7%B3%BB%E6%A8%A1%E5%9E%8B-(er-%E6%A6%82%E8%BF%B0)" tabindex="-1">5.1 实体关系模型 (ER 概述)</h3><p>数据库主要用于持久化产生的数据，不要用关系型数据库存死配置（配置统一用 YAML）。</p><ol><li><p><strong>Table: ExecutionLogs (主表)</strong></p><ul><li><code>Id</code> (PK, Guid)</li><li><code>SkillId</code> (Varchar, 索引)</li><li><code>StartTime</code> (Datetime)</li><li><code>EndTime</code> (Datetime)</li><li><code>Status</code> (Int) -&gt; Enum</li><li><code>TriggerMode</code> (Int) -&gt; Enum</li></ul></li><li><p><strong>Table: StepLogs (明细表)</strong></p><ul><li><code>Id</code> (PK, Guid)</li><li><code>ExecutionId</code> (FK -&gt; <a href="http://ExecutionLogs.Id" target="_blank">ExecutionLogs.Id</a>)</li><li><code>StepId</code> (Varchar, “step_1”)</li><li><code>ActionName</code> (Varchar)</li><li><code>DurationMs</code> (Int)</li><li><code>Status</code> (Int)</li><li><code>RawInput</code> (Text/JSON, 执行前的实际参数)</li><li><code>RawOutput</code> (Text/JSON, 执行结果或 Error Stacktrace)</li></ul></li><li><p><strong>Table: Analytics (统计报表，由定时任务聚合)</strong></p><ul><li><code>Date</code> (Date)</li><li><code>SkillId</code> (Varchar)</li><li><code>TotalExecutions</code> (Int)</li><li><code>SuccessRate</code> (Float)</li><li><code>TokensUsed</code> (Int)</li></ul></li></ol><hr /><h2 id="6.-%E6%8A%80%E6%9C%AF%E6%A0%88%E4%B8%8E%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5-(%E5%A2%9E%E5%BC%BA%E7%89%88)" tabindex="-1">6. 技术栈与工程实践 (增强版)</h2><h3 id="6.1-%E7%B2%BE%E7%A1%AE%E7%89%88%E6%9C%AC%E9%94%81%E5%AE%9A" tabindex="-1">6.1 精确版本锁定</h3><p>避免版本碎片化，规定基础框架：</p><ul><li><strong>Target Framework:</strong> <code>net8.0-windows</code></li><li><strong>WPF 模式:</strong> 采用 MVVM 模式，避免后置代码(Code-Behind)臃肿。</li></ul><h3 id="6.2-%E6%A0%B8%E5%BF%83-nuget-%E5%8C%85%E6%B8%85%E5%8D%95%E5%8F%8A%E7%94%A8%E9%80%94%E8%A1%A5%E5%85%85" tabindex="-1">6.2 核心 NuGet 包清单及用途补充</h3><pre><code class="language-xml">&lt;!-- 微软依赖注入与配置扩展，用于 Host Builder 构建 --&gt;&lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting&quot; Version=&quot;8.0.0&quot; /&gt;&lt;PackageReference Include=&quot;Microsoft.Extensions.Http.Polly&quot; Version=&quot;8.0.0&quot; /&gt; &lt;!-- 用于 HTTP 调用的弹性策略 (重试、熔断) --&gt;&lt;!-- 现代 MVVM 框架，取代 Prism，更加轻量现代 --&gt;&lt;PackageReference Include=&quot;CommunityToolkit.Mvvm&quot; Version=&quot;8.2.2&quot; /&gt;&lt;!-- 浏览器内核 --&gt;&lt;PackageReference Include=&quot;Microsoft.Web.WebView2&quot; Version=&quot;1.0.2210.55&quot; /&gt;&lt;!-- 大模型 SDK --&gt;&lt;PackageReference Include=&quot;Aliyun.SDK.DashScope&quot; Version=&quot;1.0.0&quot; /&gt;&lt;!-- 定时任务调度器，比原生 Timer 更可靠，支持 Cron 表达式 --&gt;&lt;PackageReference Include=&quot;Quartz&quot; Version=&quot;3.8.0&quot; /&gt;&lt;PackageReference Include=&quot;Quartz.Extensions.Hosting&quot; Version=&quot;3.8.0&quot; /&gt;&lt;!-- ORM 选用轻量且快速的 Dapper --&gt;&lt;PackageReference Include=&quot;Dapper&quot; Version=&quot;2.1.28&quot; /&gt;&lt;PackageReference Include=&quot;Microsoft.Data.Sqlite&quot; Version=&quot;8.0.0&quot; /&gt;</code></pre><hr /><h2 id="7.-%E8%AF%A6%E7%BB%86%E5%BC%80%E5%8F%91%E9%98%B6%E6%AE%B5%E6%8B%86%E8%A7%A3-(%E4%B8%BA-ai-%E6%89%A7%E8%A1%8C%E5%81%9A%E5%87%86%E5%A4%87)" tabindex="-1">7. 详细开发阶段拆解 (为 AI 执行做准备)</h2><p>这一部分是将原本高层次的规划，拆解为可以分配给 AI 编程助手的颗粒度 <code>Task</code>。</p><h3 id="phase-1%3A-%E5%BA%95%E5%B1%82%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD-(infrastructure)---%E9%A2%84%E8%AE%A1%E8%80%97%E6%97%B6-3-%E5%A4%A9" tabindex="-1">Phase 1: 底层基础设施 (Infrastructure) - 预计耗时 3 天</h3><ul><li><strong>Task 1.1:</strong> 搭建 <code>.sln</code> 结构与空项目约束 (UI, Core, Skills, Host)。引入必要的 NuGet 包。</li><li><strong>Task 1.2:</strong> 实现通用的依赖注入 <code>Startup</code> 注册类，并搭建基于 <code>ILogger&lt;T&gt;</code> 的日志系统（输出到控制台与文件）。</li><li><strong>Task 1.3:</strong> 设计并实现 SQLite 仓储层，包含基于 Dapper 的基础增删改查和数据库自动建表脚本（启动时如果不存在库则自动创建）。</li></ul><h3 id="phase-2%3A-%E6%A0%B8%E5%BF%83%E5%BC%95%E6%93%8E%E9%AA%A8%E6%9E%B6-(agent-engine)---%E9%A2%84%E8%AE%A1%E8%80%97%E6%97%B6-4-%E5%A4%A9" tabindex="-1">Phase 2: 核心引擎骨架 (Agent Engine) - 预计耗时 4 天</h3><ul><li><strong>Task 2.1:</strong> 定义 <code>ISkill</code>, <code>IActionTool</code>, <code>IPerception</code> 等核心接口（复制 8.1 节内容生成文件）。</li><li><strong>Task 2.2:</strong> 利用 YamlDotNet 编写 <code>SkillConfigReader</code>，支持反序列化目录下的所有 YAML 文件到 <code>SkillDefinition</code> 对象中。</li><li><strong>Task 2.3:</strong> 实现 <code>ActionFactory</code> 和反射路由，保证给一个 action_name 字符串，能返回一个 <code>IActionTool</code>。</li><li><strong>Task 2.4:</strong> 组装主心骨 <code>SkillEngine::ExecuteAsync</code> 方法。实现顺序遍历 Step、参数插值、重试抓取和上下文（StateBag）管理。</li></ul><h3 id="phase-3%3A-%E5%B7%A5%E5%85%B7%E8%83%BD%E5%8A%9B%E6%8E%A5%E5%85%A5-(skills-implementations)---%E9%A2%84%E8%AE%A1%E8%80%97%E6%97%B6-5-%E5%A4%A9" tabindex="-1">Phase 3: 工具能力接入 (Skills Implementations) - 预计耗时 5 天</h3><ul><li><strong>Task 3.1:</strong> 封装 <code>WebView2Wrapper</code>。实现原生的 Navigate, WaitForNetworkIdle, ExecuteScript 等底层 API 封装。（由于是后台进程要求，需处理好无界面的 WebView2 实例化问题，或隐藏窗体）。</li><li><strong>Task 3.2:</strong> 实现基于 DOM 的感知工具 <code>action: &quot;perception.dom&quot;</code>，包括查找元素、提取文本、判断是否存在。</li><li><strong>Task 3.3:</strong> 对接 <code>Aliyun.SDK.DashScope</code>，实现 <code>action: &quot;ai.analyze&quot;</code>，编写通用的大模型调用接口。</li></ul><h3 id="phase-4%3A-ui-%E4%B8%8E%E8%81%94%E8%B0%83-(wpf-client)---%E9%A2%84%E8%AE%A1%E8%80%97%E6%97%B6-3-%E5%A4%A9" tabindex="-1">Phase 4: UI 与联调 (WPF Client) - 预计耗时 3 天</h3><ul><li><strong>Task 4.1:</strong> 构建 WPF 主界面的导航栏 (Dashboard, Skills, Logs, Settings)。</li><li><strong>Task 4.2:</strong> 将 UI 线程与 Agent 后台处理挂钩，通过类似于 <code>Messenger.Default</code> 通知 UI 刷新执行状态进度条。</li><li><strong>Task 4.3:</strong> 联调“网易邮箱”或“RSS 阅读”的测试 YAML，跑通全链路并查看数据库写入结果。</li></ul><hr /><h2 id="8.-%E4%BB%A3%E7%A0%81%E6%8E%A5%E5%8F%A3%E8%AF%A6%E7%BB%86%E8%A7%84%E7%BA%A6-(guideline)" tabindex="-1">8. 代码接口详细规约 (Guideline)</h2><p>为确保 AI 生成的代码符合工程标准，制定以下代码约束：</p><pre><code class="language-csharp">namespace MyAgent.Core.Interfaces{    // 强制：所有的插件工具必须是无状态的单例 (Singleton) 生命周期    public interface IActionTool    {        // 返回如 &quot;browser.navigate&quot; 格式的标识        string ActionType { get; }                // 核心执行逻辑，必须支持 CancellationToken        // JToken 允许传入动态结构，工具内部在使用前映射为强类型的 DTO        Task&lt;ActionResult&gt; ExecuteAsync(SkillExecutionContext context, JToken parameters, CancellationToken cancellationToken);    }    public class ActionResult    {        public bool IsSuccess { get; set; }        public string ErrorMessage { get; set; }        // 执行产出的数据，将被引擎自动合并到 context.StateBag        public Dictionary&lt;string, object&gt; OutputData { get; set; }     }}</code></pre><h2 id="9.-%E9%AA%8C%E6%94%B6%E6%A0%87%E5%87%86%E8%A1%A5%E5%85%85" tabindex="-1">9. 验收标准补充</h2><p>增加针对开发者的技术性验收节点：</p><ol><li><strong>沙盒隔离</strong>：Skill A 的失败不应导致系统挂起，系统须能在出现未捕获异常时自动将其标记为 Fail 并继续监听下一个任务。</li><li><strong>热加载</strong>：在修改了 <code>/config/skills/</code> 目录下的某 YAML 文件后，系统应当包含文件系统变更监听 (<code>FileSystemWatcher</code>) 并自动重载规则，无需重启程序。</li><li><strong>日志溯源</strong>：控制台和 Log 文件必须保证输出包含 <code>ExecutionId</code> 关联上下文的 Trace，确保排错过程顺利。</li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[ 用 Python 打造一个支持流式播放的文本转语音工具]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/yong-python-da-zao-yi-ge-zhi-chi-liu-shi-bo-fang-de-wen-ben-zhuan-yu-yin-gong-ju" />
                <id>tag:https://maifeipin.com,2026-02-13:yong-python-da-zao-yi-ge-zhi-chi-liu-shi-bo-fang-de-wen-ben-zhuan-yu-yin-gong-ju</id>
                <published>2026-02-13T14:34:34+08:00</published>
                <updated>2026-02-13T14:50:25+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p>支持 EPUB / MOBI / PDF / DOCX 等 7 种格式，双缓冲无缝播放，完全免费</p></blockquote><hr /><h2 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E9%80%A0%E8%BF%99%E4%B8%AA%E8%BD%AE%E5%AD%90%EF%BC%9F" tabindex="-1">为什么要造这个轮子？</h2><p>市面上的 TTS（Text-to-Speech）工具要么收费，要么语音质量差，要么只支持纯文本。我的需求很简单：</p><ol><li><strong>语音质量要好</strong> — 接近真人朗读</li><li><strong>支持长文本</strong> — 一本小说几十万字，不能一次性生成</li><li><strong>支持电子书格式</strong> — EPUB、MOBI 这些主流格式</li><li><strong>免费</strong> — 不想为听书付月费</li></ol><h2 id="%E4%BA%8E%E6%98%AF%EF%BC%8Cedgettsplayer-%E8%AF%9E%E7%94%9F%E4%BA%86%E3%80%82" tabindex="-1">于是，<strong>EdgeTTSPlayer</strong> 诞生了。<br /><img src="/upload/2026/02/image-1770965412761.png" alt="image-1770965412761" /></h2><h2 id="%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B" tabindex="-1">技术选型</h2><table><thead><tr><th>组件</th><th>选择</th><th>理由</th></tr></thead><tbody><tr><td>TTS 引擎</td><td><a href="https://github.com/rany2/edge-tts" target="_blank">edge-tts</a></td><td>微软神经网络 TTS，14+ 中文语音，免费无限制</td></tr><tr><td>音频播放</td><td><a href="https://www.pygame.org/" target="_blank">pygame.mixer</a></td><td>跨平台 MP3 播放，支持状态检测</td></tr><tr><td>GUI 框架</td><td>Tkinter + ttk</td><td>Python 内置，零依赖部署</td></tr><tr><td>EPUB 解析</td><td><a href="https://github.com/aerkalov/ebooklib" target="_blank">ebooklib</a></td><td>成熟的 EPUB 读写库</td></tr><tr><td>MOBI 解析</td><td><a href="https://pypi.org/project/mobi/" target="_blank">mobi</a></td><td>解包 MOBI → HTML → 纯文本</td></tr><tr><td>PDF 提取</td><td><a href="https://pypi.org/project/PyPDF2/" target="_blank">PyPDF2</a></td><td>轻量级 PDF 文本提取</td></tr><tr><td>DOCX 解析</td><td><a href="https://python-docx.readthedocs.io/" target="_blank">python-docx</a></td><td>Word 文档段落提取</td></tr></tbody></table><p>最初我用的是 <code>pyttsx3</code>（离线 TTS），但中文语音效果太机械了。切换到 <code>edge-tts</code> 后，差距是质的飞跃 — 它调用的是 Microsoft Edge 浏览器内置的神经网络 TTS 服务，完全免费，语音质量接近真人。</p><hr /><h2 id="%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84%EF%BC%9A%E6%96%AD%E5%8F%A5-%2B-%E5%8F%8C%E7%BC%93%E5%86%B2%E6%B5%81%E5%BC%8F%E6%92%AD%E6%94%BE" tabindex="-1">核心架构：断句 + 双缓冲流式播放</h2><p>长文本直接生成一整段音频，既慢又占内存。我的方案是<strong>断句分片 + 双缓冲预加载</strong>：</p><pre><code class="language-">               ┌──────────────────────────────────────────────┐               │              文本处理流水线                    │               │                                              │  文件输入 ──→ │ read_book_file() ──→ split_text_to_chunks()  │  (7种格式)    │   格式解析              按标点断句             │               └──────────────┬───────────────────────────────┘                              │                              ▼               ┌──────────────────────────────────────────────┐               │           双缓冲播放引擎                      │               │                                              │               │  ┌─────────────┐    ┌──────────────────┐    │               │  │ 播放 chunk[n]│    │ 生成 chunk[n+1]   │    │               │  │ pygame.mixer│◀──▶│ edge-tts + asyncio│    │               │  └──────┬──────┘    └──────────────────┘    │               │         │ 播完后自动删除临时 MP3              │               │         ▼                                    │               │    自动切换到 chunk[n+1]                      │               └──────────────────────────────────────────────┘</code></pre><p><img src="/upload/2026/02/image-1770965148715.png" alt="image-1770965148715" /></p><h3 id="1.-%E6%99%BA%E8%83%BD%E6%96%AD%E5%8F%A5%EF%BC%9Asplit_text_to_chunks()" tabindex="-1">1. 智能断句：<code>split_text_to_chunks()</code></h3><p>不能简单按固定字数硬切 — 那样会把句子切断，听起来很别扭。我采用了<strong>两级断句策略</strong>：</p><pre><code class="language-python"># 强分隔符：句号、叹号、问号、分号SENTENCE_DELIMITERS = re.compile(r&#39;(?&lt;=[。！？；…!?;])|(?&lt;=\n)&#39;)# 弱分隔符：逗号、顿号CLAUSE_DELIMITERS = re.compile(r&#39;(?&lt;=[，、,])&#39;)</code></pre><p><strong>算法逻辑：</strong></p><ol><li>先按强分隔符拆分成句子</li><li>将句子攒入 buffer，直到接近 <code>max_length</code>（默认 200 字）</li><li>如果单个句子超长，再按弱分隔符（逗号）二次拆分</li><li>最坏情况下按字数硬切（保证不会死循环）</li></ol><p>实际效果（<code>max_length=30</code>）：</p><pre><code class="language-">原文: 今天天气晴朗，万里无云。我出门去散步，走了很长一段路。      到了公园里，看到很多人在锻炼身体！有的跑步，有的打太极拳；      还有些人在唱歌。真是一个美好的早晨。断句结果:  [1] (27字) 今天天气晴朗，万里无云。我出门去散步，走了很长一段路。  [2] (29字) 到了公园里，看到很多人在锻炼身体！有的跑步，有的打太极拳；  [3] (18字) 还有些人在唱歌。真是一个美好的早晨。</code></pre><h3 id="2.-%E5%8F%8C%E7%BC%93%E5%86%B2%E6%92%AD%E6%94%BE%EF%BC%9A%E8%BE%B9%E6%92%AD%E8%BE%B9%E7%94%9F%E6%88%90" tabindex="-1">2. 双缓冲播放：边播边生成</h3><p>这是整个工具的核心设计。如果生成一段、播放一段、再生成下一段，每次切换都会有几秒的空白停顿。</p><p><strong>双缓冲方案：</strong></p><pre><code class="language-python"># 播放 chunk[n] 的同时，在另一个线程中预生成 chunk[n+1]gen_thread = threading.Thread(target=_gen_next, daemon=True)gen_thread.start()# 播放当前片段pygame.mixer.music.load(current_path)pygame.mixer.music.play()# 等播放完毕后，下一个片段已经生成好了while pygame.mixer.music.get_busy():    if self._playback_stop.is_set():        pygame.mixer.music.stop()        return    pygame.time.wait(100)</code></pre><p><strong>效果：</strong> 片段之间的切换几乎感觉不到停顿，因为下一段音频在上一段播放期间就已生成完毕。</p><h3 id="3.-%E8%87%AA%E5%8A%A8%E6%B8%85%E7%90%86%EF%BC%9A%E4%B8%8D%E7%95%99%E4%B8%B4%E6%97%B6%E6%96%87%E4%BB%B6" tabindex="-1">3. 自动清理：不留临时文件</h3><p>每次播放会在系统临时目录创建一个独立文件夹，每个片段生成为 <code>chunk_0.mp3</code>、<code>chunk_1.mp3</code> …</p><ul><li>播放完成的片段<strong>立即删除</strong>（<code>pygame.mixer.music.unload()</code> → <code>os.remove()</code>）</li><li>用户点击停止或播放完毕后，<strong>整个目录删除</strong>（<code>shutil.rmtree()</code>）</li><li>窗口关闭时也会触发清理</li></ul><pre><code class="language-python">def _cleanup_temp_dir(self):    if self._temp_dir and os.path.isdir(self._temp_dir):        shutil.rmtree(self._temp_dir, ignore_errors=True)        self._temp_dir = None</code></pre><hr /><h2 id="%E5%A4%9A%E6%A0%BC%E5%BC%8F%E6%94%AF%E6%8C%81%EF%BC%9A%E4%B8%80%E4%B8%AA%E5%87%BD%E6%95%B0%E6%90%9E%E5%AE%9A" tabindex="-1">多格式支持：一个函数搞定</h2><p><code>read_book_file()</code> 根据文件扩展名自动选择解析策略：</p><pre><code class="language-python">def read_book_file(file_path):    ext = Path(file_path).suffix.lower()    if ext in (&#39;.txt&#39;, &#39;.md&#39;):        return path.read_text(encoding=&#39;utf-8&#39;)    if ext in (&#39;.html&#39;, &#39;.htm&#39;):        soup = BeautifulSoup(html, &#39;html.parser&#39;)        return soup.get_text(separator=&#39;\n&#39;, strip=True)    if ext == &#39;.epub&#39;:        book = epub.read_epub(str(path))        # 遍历所有章节，提取纯文本        ...    if ext == &#39;.mobi&#39;:        # mobi 解包 → 找到 HTML → BeautifulSoup 提取        ...    if ext == &#39;.pdf&#39;:        reader = PdfReader(str(path))        # 逐页提取文本        ...    if ext == &#39;.docx&#39;:        doc = DocxDocument(str(path))        # 提取所有段落        ...</code></pre><p>MOBI 格式比较特殊 — 它是亚马逊的私有格式，需要先解包到临时目录，找到里面的 HTML 文件，再用 BeautifulSoup 提取文本。解包后的临时目录也会在 <code>finally</code> 块中自动清理。</p><hr /><h2 id="%E4%BD%BF%E7%94%A8%E6%95%88%E6%9E%9C" tabindex="-1">使用效果</h2><p>运行 <code>python main.py</code> 启动应用后：</p><ol><li>点击 <strong>选择文件</strong> — 支持 TXT/MD/HTML/EPUB/MOBI/PDF/DOCX</li><li>选择中文语音（14+ 可选）、调整语速和音量</li><li>点击 <strong>▶ 播放</strong> — 自动断句并流式播放</li><li>状态栏实时显示 <code>▶ 正在播放 3/142 片段...</code></li><li>随时点击 <strong>■ 停止</strong>，临时文件自动清理</li></ol><p>也可以点击 <strong>转换为MP3</strong> 导出完整音频文件，或 <strong>批量转换</strong> 一次处理多个文件。</p><hr /><h2 id="%E5%90%8E%E7%BB%AD%E8%B7%AF%E7%BA%BF%E5%9B%BE-%F0%9F%97%BA%EF%B8%8F" tabindex="-1">后续路线图 🗺️</h2><p>当前版本（v1.0）已经可以日常使用，但还有一些有价值的功能计划中：</p><table><thead><tr><th>功能</th><th>描述</th><th>状态</th></tr></thead><tbody><tr><td>📖 文本高亮同步</td><td>播放时自动高亮当前正在朗读的句子</td><td>计划中</td></tr><tr><td>📌 记忆播放位置</td><td>关闭后重新打开，从上次停止的地方继续</td><td>计划中</td></tr><tr><td>🌍 多语言支持</td><td>扩展到英文、日文等其他语音</td><td>计划中</td></tr><tr><td>📦 打包为 EXE</td><td>使用 PyInstaller 打包成无需 Python 环境的独立应用</td><td>计划中</td></tr></tbody></table><hr /><h2 id="%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B" tabindex="-1">快速上手</h2><pre><code class="language-bash">git clone https://github.com/maifeipin/EdgeTTSPlayer.gitcd EdgeTTSPlayerpip install -r requirements.txtpython main.py</code></pre><p><strong>依赖：</strong> Python 3.10+ | 需要网络连接（Microsoft Edge 在线 TTS 服务，免费无限制）</p><hr /><h2 id="%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80" tabindex="-1">项目地址</h2><p>🔗 GitHub: <a href="https://github.com/maifeipin/EdgeTTSPlayer" target="_blank">maifeipin/EdgeTTSPlayer</a></p><p>欢迎 Star ⭐ 和提 Issue！</p><hr /><p><em>作者：maifeipin &amp; Antigravity AI</em><br /><em>日期：2026 年 2 月 13 日</em><br /><em>技术栈：Python · edge-tts · pygame · Tkinter</em></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[WinForm 对接 Keycloak SSO 技术方案]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/winform-dui-jie-keycloaksso-ji-shu-fang-an" />
                <id>tag:https://maifeipin.com,2026-01-05:winform-dui-jie-keycloaksso-ji-shu-fang-an</id>
                <published>2026-01-05T13:03:56+08:00</published>
                <updated>2026-01-05T15:07:21+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p>本文档详细阐述旧版 WinForm 桌面应用如何对接公司现有 Keycloak 统一认证平台，实现单点登录（SSO）。</p></blockquote><hr /><h2 id="%E4%B8%80%E3%80%81%E8%83%8C%E6%99%AF%E4%B8%8E%E7%9B%AE%E6%A0%87" tabindex="-1">一、背景与目标</h2><h3 id="1.1-%E7%8E%B0%E7%8A%B6" tabindex="-1">1.1 现状</h3><ul><li>公司已部署 <strong>Keycloak 统一认证平台</strong>，多个系统已完成对接</li><li>现有 WinForm 桌面应用采用独立的用户名/密码登录</li><li>需要将 WinForm 纳入 SSO 体系，实现&quot;一处登录，处处通行&quot;</li></ul><h3 id="1.2-%E7%9B%AE%E6%A0%87" tabindex="-1">1.2 目标</h3><table><thead><tr><th>目标</th><th>说明</th></tr></thead><tbody><tr><td>统一认证</td><td>WinForm 使用公司统一账号登录</td></tr><tr><td>用户体验</td><td>如果用户已在浏览器登录过其他系统，WinForm 可自动完成登录（免输密码）</td></tr><tr><td>安全合规</td><td>采用业界标准 OAuth 2.0 + PKCE 协议，满足安全审计要求</td></tr></tbody></table><hr /><h2 id="%E4%BA%8C%E3%80%81%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88%E6%A6%82%E8%BF%B0" tabindex="-1">二、技术方案概述</h2><h3 id="2.1-%E9%80%89%E7%94%A8%E5%8D%8F%E8%AE%AE%EF%BC%9Aoauth-2.0-%2B-oidc-%2B-pkce" tabindex="-1">2.1 选用协议：OAuth 2.0 + OIDC + PKCE</h3><p>对于桌面应用，业界标准推荐使用 <strong>Authorization Code Flow with PKCE</strong>：</p><pre><code class="language-">┌─────────────────────────────────────────────────────────────────────────┐│                           为什么选择 PKCE？                              │├─────────────────────────────────────────────────────────────────────────┤│                                                                         ││  传统 Web 应用可以使用 client_secret（存在服务器端），但桌面应用不行：     ││                                                                         ││  ❌ 桌面应用的代码可被反编译，client_secret 无法保密                      ││  ❌ 传统 Implicit Flow 已被 OAuth 2.1 废弃（不安全）                      ││                                                                         ││  ✅ PKCE 方案：每次登录生成临时密钥对，无需存储长期密钥                    ││                                                                         │└─────────────────────────────────────────────────────────────────────────┘</code></pre><h3 id="2.2-%E6%9E%B6%E6%9E%84%E5%9B%BE" tabindex="-1">2.2 架构图</h3><pre><code class="language-">    ┌─────────────┐                              ┌─────────────────────┐    │  WinForm    │                              │  Keycloak Server    │    │  桌面应用    │                              │  (公司已部署)        │    └──────┬──────┘                              └──────────┬──────────┘           │                                                │           │  ① 用户点击登录 → 打开系统浏览器                  │           │ ───────────────────────────────────────────────&gt;│           │                                                │           │  ② 用户在浏览器登录（输入用户名密码/扫码/SSO）     │           │                                                │           │  ③ 登录成功 → 浏览器跳转回本地回调地址             │           │ &lt;───────────────────────────────────────────────│           │                                                │           │  ④ WinForm 用授权码换取 Token                    │           │ ───────────────────────────────────────────────&gt;│           │                                                │           │  ⑤ 返回 access_token、refresh_token             │           │ &lt;───────────────────────────────────────────────│           │                                                │           │  ⑥ 携带 Token 调用业务 API                       │           │ ─────────────────────────────────────────────────────────────&gt;           │                                                              │           │                                      ┌────────────────────────┐           │                                      │      后端 API 服务      │           │ &lt;─────────────────────────────────────────────────────────────│           │  ⑦ 返回业务数据                       └────────────────────────┘</code></pre><hr /><h2 id="%E4%B8%89%E3%80%81%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%85%8D%E7%BD%AE%EF%BC%88keycloak-%E7%AE%A1%E7%90%86%E5%91%98%E6%93%8D%E4%BD%9C%EF%BC%89" tabindex="-1">三、服务端配置（Keycloak 管理员操作）</h2><blockquote><p><strong>注意</strong>：Keycloak 服务端已部署完成，只需新增一个 Client 配置即可。</p></blockquote><h3 id="3.1-%E6%96%B0%E5%BB%BA-client" tabindex="-1">3.1 新建 Client</h3><p>在 Keycloak 管理控制台创建新客户端：</p><table><thead><tr><th>配置项</th><th>值</th><th>说明</th></tr></thead><tbody><tr><td><strong>Client ID</strong></td><td><code>winform-legacy-app</code></td><td>客户端唯一标识</td></tr><tr><td><strong>Client Protocol</strong></td><td><code>openid-connect</code></td><td>使用 OIDC 协议</td></tr><tr><td><strong>Access Type</strong></td><td><code>public</code></td><td>公开客户端（桌面应用无法保密）</td></tr><tr><td><strong>Standard Flow Enabled</strong></td><td>✅ ON</td><td>启用授权码流程</td></tr><tr><td><strong>Valid Redirect URIs</strong></td><td><code>http://localhost:18080/*</code></td><td>本地回调地址（端口可自定）</td></tr><tr><td><strong>PKCE Code Challenge Method</strong></td><td><code>S256</code></td><td><strong>必须启用</strong></td></tr></tbody></table><h3 id="3.2-%E8%8E%B7%E5%8F%96%E5%85%B3%E9%94%AE%E7%AB%AF%E7%82%B9" tabindex="-1">3.2 获取关键端点</h3><p>从 Keycloak 发现文档获取（无需手动配置）：</p><pre><code class="language-">发现文档地址：https://{keycloak-host}/realms/{realm}/.well-known/openid-configuration常用端点：├── 授权端点: /realms/{realm}/protocol/openid-connect/auth├── Token端点: /realms/{realm}/protocol/openid-connect/token├── 用户信息: /realms/{realm}/protocol/openid-connect/userinfo└── 登出端点: /realms/{realm}/protocol/openid-connect/logout</code></pre><hr /><h2 id="%E5%9B%9B%E3%80%81%E5%AE%8C%E6%95%B4%E4%BA%A4%E4%BA%92%E6%B5%81%E7%A8%8B%E8%AF%A6%E8%A7%A3" tabindex="-1">四、完整交互流程详解</h2><h3 id="4.1-%E6%B5%81%E7%A8%8B%E6%97%B6%E5%BA%8F%E5%9B%BE" tabindex="-1">4.1 流程时序图</h3><pre><code class="language-">  ┌──────────┐       ┌──────────┐       ┌──────────┐       ┌──────────┐  │ WinForm  │       │ 系统浏览器 │       │ Keycloak │       │ 后端API  │  └────┬─────┘       └────┬─────┘       └────┬─────┘       └────┬─────┘       │                  │                  │                  │  【第1步】用户点击&quot;登录&quot;按钮       │ 生成 PKCE 密钥对   │                  │                  │       │ (code_verifier,   │                  │                  │       │  code_challenge)  │                  │                  │       │                  │                  │                  │  【第2步】启动本地 HTTP 监听 + 打开浏览器       │ 启动 HttpListener │                  │                  │       │ 监听 localhost    │                  │                  │       │ ────打开浏览器───&gt;│                  │                  │       │                  │ ──GET /auth─────&gt;│                  │       │                  │  (带 PKCE 公钥)   │                  │       │                  │                  │                  │  【第3步】Keycloak 展示登录页面       │                  │ &lt;──返回登录页────│                  │       │                  │                  │                  │       │      【用户操作】在浏览器中输入用户名密码，点击登录         │       │                  │                  │                  │       │                  │ ──POST 登录─────&gt;│                  │       │                  │                  │ 验证凭据          │       │                  │                  │ 创建 Session      │       │                  │                  │ 生成授权码        │       │                  │ &lt;──302 重定向────│                  │       │                  │                  │                  │  【第4步】浏览器跳转到本地回调地址       │ &lt;─GET /callback──│                  │                  │       │  ?code=xxx       │                  │                  │       │ 返回&quot;登录成功&quot;页面─&gt;│                  │                  │       │                  │                  │                  │  【第5步】用授权码 + PKCE私钥换取 Token（核心安全验证）       │ ────────────POST /token────────────&gt;│                  │       │   code=xxx                          │                  │       │   code_verifier=原始PKCE私钥 ←←←←←←│ PKCE验证          │       │ &lt;───────────返回 Token──────────────│                  │       │                  │                  │                  │  【第6步】携带 Token 调用业务 API       │ ─────────────────GET /api/data─────────────────────────&gt;│       │   Authorization: Bearer {access_token}                  │       │ &lt;─────────────────返回业务数据───────────────────────────│       │                  │                  │                  │</code></pre><h3 id="4.2-%E5%90%84%E6%AD%A5%E9%AA%A4%E8%AF%A6%E8%A7%A3" tabindex="-1">4.2 各步骤详解</h3><h4 id="%E7%AC%AC1%E6%AD%A5%EF%BC%9A%E7%94%9F%E6%88%90-pkce-%E5%AF%86%E9%92%A5%E5%AF%B9" tabindex="-1">第1步：生成 PKCE 密钥对</h4><p>PKCE 的核心是<strong>一次性密钥对</strong>，每次登录重新生成：</p><pre><code class="language-">┌────────────────────────────────────────────────────────────────────┐│                         PKCE 密钥对                                 │├────────────────────────────────────────────────────────────────────┤│                                                                    ││  code_verifier（私钥，保密！仅存于内存）                             ││  ────────────────────────────────────                              ││  随机字符串，例如: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk      ││                                                                    ││                    │                                               ││                    │ SHA256 哈希 + Base64URL 编码                   ││                    ▼                                               ││                                                                    ││  code_challenge（公钥，发送给 Keycloak）                            ││  ─────────────────────────────────────                             ││  哈希结果，例如: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM        ││                                                                    ││  ┌────────────────────────────────────────────────────────────┐    ││  │ 【安全原理】                                                │    ││  │ code_verifier 始终在 WinForm 内存中，从不经过网络传输        │    ││  │ 即使攻击者截获 code_challenge，也无法反推出 code_verifier    │    ││  │ （SHA256 是单向哈希，不可逆）                                │    ││  └────────────────────────────────────────────────────────────┘    ││                                                                    │└────────────────────────────────────────────────────────────────────┘</code></pre><h4 id="%E7%AC%AC2%E6%AD%A5%EF%BC%9A%E6%9E%84%E5%BB%BA%E6%8E%88%E6%9D%83-url" tabindex="-1">第2步：构建授权 URL</h4><pre><code class="language-">https://keycloak.company.com/realms/corp/protocol/openid-connect/auth    ?client_id=winform-legacy-app              ← 客户端标识    &amp;response_type=code                        ← 要求返回授权码    &amp;redirect_uri=http://localhost:18080/callback  ← 回调地址    &amp;scope=openid profile email                ← 请求的权限    &amp;state=随机字符串                           ← 防CSRF攻击    &amp;code_challenge=E9Melhoa2OwvFrEMTJgu...    ← PKCE公钥    &amp;code_challenge_method=S256                ← 哈希算法</code></pre><h4 id="%E7%AC%AC3%E6%AD%A5%EF%BC%9A%E7%94%A8%E6%88%B7%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E6%93%8D%E4%BD%9C" tabindex="-1">第3步：用户在浏览器中操作</h4><p>用户看到的界面（公司统一登录页）：</p><pre><code class="language-">┌──────────────────────────────────────────────────────────────┐│  🔒 https://keycloak.company.com/realms/corp/login           │├──────────────────────────────────────────────────────────────┤│                                                              ││            ┌────────────────────────────────┐                ││            │      🏢 XX公司统一登录平台      │                ││            │                                │                ││            │  ┌────────────────────────┐    │                ││            │  │ 用户名/工号            │    │                ││            │  └────────────────────────┘    │                ││            │  ┌────────────────────────┐    │                ││            │  │ 密码                   │    │                ││            │  └────────────────────────┘    │                ││            │                                │                ││            │  ┌────────────────────────┐    │                ││            │  │        登  录          │    │                ││            │  └────────────────────────┘    │                ││            │                                │                ││            │  ─────── 或使用 ───────        │                ││            │  🟢 企业微信扫码  📱 钉钉      │                ││            │                                │                ││            └────────────────────────────────┘                ││                                                              │└──────────────────────────────────────────────────────────────┘</code></pre><p><strong>SSO 优势体现</strong>：</p><ul><li>如果用户已在浏览器中登录过 OA/CRM 等其他系统，此步骤自动跳过</li><li>浏览器会直接携带已有的 Keycloak Session 完成认证</li></ul><h4 id="%E7%AC%AC4~5%E6%AD%A5%EF%BC%9Apkce-%E4%BF%A1%E4%BB%BB%E9%AA%8C%E8%AF%81%EF%BC%88%E6%A0%B8%E5%BF%83%E5%AE%89%E5%85%A8%E6%9C%BA%E5%88%B6%EF%BC%89" tabindex="-1">第4~5步：PKCE 信任验证（核心安全机制）</h4><pre><code class="language-">┌──────────────────────────────────────────────────────────────────┐│                    为什么 PKCE 能证明请求可信？                    │├──────────────────────────────────────────────────────────────────┤│                                                                  ││   【第2步】WinForm 生成密钥对                                     ││   ───────────────────────────                                    ││   code_verifier = &quot;dBjftJeZ4CVP...&quot;  ← 保存在内存                ││   code_challenge = SHA256(code_verifier) = &quot;E9Melhoa2Owv...&quot;    ││                              ↓                                   ││                     发送给 Keycloak 存储                          ││                                                                  ││   【第5步】换 Token 时验证                                        ││   ────────────────────────                                       ││   WinForm 发送: code_verifier = &quot;dBjftJeZ4CVP...&quot;                ││                              ↓                                   ││   Keycloak 计算: SHA256(&quot;dBjftJeZ4CVP...&quot;) = &quot;E9Melhoa2Owv...&quot;   ││                              ↓                                   ││   对比: 计算结果 == 存储的 code_challenge ?                       ││                              ↓                                   ││   ✅ 匹配 → 证明是同一个客户端发起的请求 → 颁发 Token              ││   ❌ 不匹配 → 拒绝（可能是攻击者窃取了 code）                      ││                                                                  ││   ┌────────────────────────────────────────────────────────┐     ││   │ 【关键结论】                                            │     ││   │ 即使攻击者在网络中截获了 authorization_code，            │     ││   │ 由于不知道 code_verifier，也无法通过 PKCE 验证。         │     ││   │ code_verifier 从未在网络上传输过！                       │     ││   └────────────────────────────────────────────────────────┘     ││                                                                  │└──────────────────────────────────────────────────────────────────┘</code></pre><hr /><h2 id="%E4%BA%94%E3%80%81token-%E8%AF%B4%E6%98%8E" tabindex="-1">五、Token 说明</h2><h3 id="5.1-keycloak-%E8%BF%94%E5%9B%9E%E7%9A%84-token-%E7%BB%93%E6%9E%84" tabindex="-1">5.1 Keycloak 返回的 Token 结构</h3><pre><code class="language-json">{    &quot;access_token&quot;: &quot;eyJhbGciOiJSUzI1NiIs...&quot;,   // 访问令牌    &quot;expires_in&quot;: 300,                           // 有效期 5 分钟    &quot;refresh_expires_in&quot;: 1800,                  // 刷新令牌有效期 30 分钟    &quot;refresh_token&quot;: &quot;eyJhbGciOiJIUzI1...&quot;,     // 刷新令牌    &quot;token_type&quot;: &quot;Bearer&quot;,                      // 令牌类型    &quot;id_token&quot;: &quot;eyJhbGciOiJSUzI1NiIs...&quot;,      // 身份令牌    &quot;session_state&quot;: &quot;a1b2c3-...&quot;,              // 会话ID    &quot;scope&quot;: &quot;openid profile email&quot;              // 授权范围}</code></pre><h3 id="5.2-%E4%B8%89%E7%A7%8D-token-%E7%9A%84%E7%94%A8%E9%80%94" tabindex="-1">5.2 三种 Token 的用途</h3><table><thead><tr><th>Token 类型</th><th>用途</th><th>说明</th></tr></thead><tbody><tr><td><strong>access_token</strong></td><td>调用后端 API</td><td>放在 HTTP Header 中：<code>Authorization: Bearer xxx</code></td></tr><tr><td><strong>id_token</strong></td><td>获取用户信息</td><td>解析 JWT 可得到用户名、邮箱、角色等</td></tr><tr><td><strong>refresh_token</strong></td><td>刷新 access_token</td><td>access_token 过期后，用 refresh_token 换新的</td></tr></tbody></table><h3 id="5.3-id_token-%E8%A7%A3%E6%9E%90%E7%A4%BA%E4%BE%8B" tabindex="-1">5.3 id_token 解析示例</h3><p>id_token 是 JWT 格式，Base64 解码后：</p><pre><code class="language-json">{    &quot;sub&quot;: &quot;f8a7b6c5-1234-5678-9abc-def012345678&quot;,  // 用户唯一ID    &quot;preferred_username&quot;: &quot;zhangsan&quot;,               // 用户名    &quot;name&quot;: &quot;张三&quot;,                                  // 显示名    &quot;email&quot;: &quot;zhangsan@company.com&quot;,                // 邮箱    &quot;realm_access&quot;: {        &quot;roles&quot;: [&quot;员工&quot;, &quot;项目经理&quot;]                // 用户角色    }}</code></pre><hr /><h2 id="%E5%85%AD%E3%80%81winform-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%BC%80%E5%8F%91%E8%A6%81%E7%82%B9" tabindex="-1">六、WinForm 客户端开发要点</h2><h3 id="6.1-%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B" tabindex="-1">6.1 技术选型</h3><table><thead><tr><th>组件</th><th>推荐方案</th><th>备注</th></tr></thead><tbody><tr><td>OIDC 库</td><td><code>IdentityModel.OidcClient</code></td><td>NuGet 包，封装了 PKCE 流程</td></tr><tr><td>HTTP 客户端</td><td><code>HttpClient</code></td><td>.NET 内置</td></tr><tr><td>Token 存储</td><td><code>ProtectedData.Protect()</code></td><td>Windows DPAPI 加密</td></tr><tr><td>浏览器交互</td><td>系统浏览器 + HttpListener</td><td>或使用 WebView2 内嵌</td></tr></tbody></table><h3 id="6.2-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81%E7%BB%93%E6%9E%84" tabindex="-1">6.2 核心代码结构</h3><pre><code class="language-csharp">// 使用 IdentityModel.OidcClient（推荐）public class KeycloakAuthService{    private readonly OidcClient _client;        public KeycloakAuthService()    {        var options = new OidcClientOptions        {            Authority = &quot;https://keycloak.company.com/realms/corp&quot;,            ClientId = &quot;winform-legacy-app&quot;,            Scope = &quot;openid profile email&quot;,            RedirectUri = &quot;http://localhost:18080/callback&quot;,            Browser = new SystemBrowser(18080)  // 自动处理本地回调        };        _client = new OidcClient(options);    }        public async Task&lt;LoginResult&gt; LoginAsync()    {        // 一行代码完成整个 OIDC + PKCE 流程        return await _client.LoginAsync();    }        public async Task&lt;RefreshTokenResult&gt; RefreshAsync(string refreshToken)    {        return await _client.RefreshTokenAsync(refreshToken);    }}</code></pre><h3 id="6.3-token-%E5%AE%89%E5%85%A8%E5%AD%98%E5%82%A8" tabindex="-1">6.3 Token 安全存储</h3><pre><code class="language-csharp">// 使用 Windows DPAPI 加密存储（推荐）public static void SaveToken(string token){    var data = Encoding.UTF8.GetBytes(token);    var encrypted = ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);    File.WriteAllBytes(tokenPath, encrypted);}public static string LoadToken(){    var encrypted = File.ReadAllBytes(tokenPath);    var data = ProtectedData.Unprotect(encrypted, null, DataProtectionScope.CurrentUser);    return Encoding.UTF8.GetString(data);}</code></pre><hr /><h2 id="%E4%B8%83%E3%80%81%E5%AE%89%E5%85%A8%E6%80%A7%E5%88%86%E6%9E%90" tabindex="-1">七、安全性分析</h2><h3 id="7.1-%E6%8A%B5%E5%BE%A1%E7%9A%84%E6%94%BB%E5%87%BB%E7%B1%BB%E5%9E%8B" tabindex="-1">7.1 抵御的攻击类型</h3><table><thead><tr><th>攻击类型</th><th>防护机制</th><th>说明</th></tr></thead><tbody><tr><td><strong>授权码窃取</strong></td><td>PKCE</td><td>攻击者没有 code_verifier，无法换取 Token</td></tr><tr><td><strong>钓鱼攻击</strong></td><td>系统浏览器 + HTTPS</td><td>用户可见真实 URL，证书验证</td></tr><tr><td><strong>CSRF 攻击</strong></td><td>state 参数</td><td>WinForm 验证 state 一致性</td></tr><tr><td><strong>Token 泄露</strong></td><td>短有效期 + HTTPS</td><td>access_token 仅 5 分钟有效</td></tr><tr><td><strong>本地存储泄露</strong></td><td>DPAPI 加密</td><td>Token 加密存储，与用户账户绑定</td></tr></tbody></table><h3 id="7.2-%E4%BF%A1%E4%BB%BB%E9%93%BE%E5%AE%8C%E6%95%B4%E6%80%A7" tabindex="-1">7.2 信任链完整性</h3><pre><code class="language-">证明最终拿到 Token 的，一定是最初发起登录的那个 WinForm 程序：   发起登录的程序                        换 Token 的程序        │                                    │        │  持有 code_verifier               │  提供 code_verifier        │         │                          │         │        │         ▼                          │         ▼        │  SHA256 → code_challenge           │  code_verifier        │         │                          │         │        │         ▼ 存储于 Keycloak          │         ▼ Keycloak 计算        │  stored_challenge ════════════════ computed_challenge        │                                    │        │                                    ▼        │                          两者相等？ → ✅ 是同一个程序        │                                    │        └────────────────────────────────────┘</code></pre><hr /><h2 id="%E5%85%AB%E3%80%81%E5%AE%9E%E6%96%BD%E8%AE%A1%E5%88%92" tabindex="-1">八、实施计划</h2><h3 id="8.1-%E9%87%8C%E7%A8%8B%E7%A2%91" tabindex="-1">8.1 里程碑</h3><p><img src="/upload/2026/01/image-1767596824077.png" alt="image-1767596824077" /></p><h3 id="8.2-%E4%BE%9D%E8%B5%96%E9%A1%B9" tabindex="-1">8.2 依赖项</h3><table><thead><tr><th>依赖项</th><th>负责人</th><th>状态</th></tr></thead><tbody><tr><td>Keycloak 新建 Client</td><td>运维/管理员</td><td>待确认</td></tr><tr><td>回调端口防火墙策略</td><td>网络管理员</td><td>待确认</td></tr><tr><td>后端 API Token 验证</td><td>后端开发</td><td>待确认</td></tr></tbody></table><hr /><h2 id="%E4%B9%9D%E3%80%81%E6%80%BB%E7%BB%93" tabindex="-1">九、总结</h2><h3 id="%E6%A0%B8%E5%BF%83%E8%A6%81%E7%82%B9" tabindex="-1">核心要点</h3><ol><li><strong>协议选择</strong>：采用 OAuth 2.0 + OIDC + PKCE，业界标准，安全可靠</li><li><strong>服务端零开发</strong>：Keycloak 已部署，仅需新建 Client 配置</li><li><strong>客户端改造</strong>：WinForm 对接标准 OIDC 流程，使用成熟类库</li><li><strong>安全保障</strong>：PKCE 机制确保授权码不可被窃取利用</li></ol><h3 id="%E6%94%B6%E7%9B%8A" tabindex="-1">收益</h3><ul><li>✅ 统一账号体系，与公司其他系统打通</li><li>✅ 支持 SSO，提升用户体验</li><li>✅ 符合安全合规要求</li><li>✅ 后续系统对接成本降低</li></ul><hr /><p><em>文档版本：1.0</em><br /><em>更新日期：2026-01-05</em></p><p><a href="https://file.maifeipin.com/api/public/dl/CoSeANCR/KeycloakWinFormDemo.7z" target="_blank">测试成功 Demo</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从 RSS 到智能推荐：构建 AI 驱动的内容处理流水线]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/cong-rss-dao-zhi-neng-tui-jian--gou-jian-ai-qu-dong-de-nei-rong-chu-li-liu-shui-xian" />
                <id>tag:https://maifeipin.com,2026-01-02:cong-rss-dao-zhi-neng-tui-jian--gou-jian-ai-qu-dong-de-nei-rong-chu-li-liu-shui-xian</id>
                <published>2026-01-02T20:06:14+08:00</published>
                <updated>2026-01-02T20:55:33+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p>本文详细介绍如何基于 .NET 8 + Vue 3 + MongoDB + Qdrant 构建一套完整的 AI 内容处理流水线，实现从 RSS 源抓取到语义搜索的全链路自动化。</p></blockquote><h2 id="%F0%9F%93%8B-%E7%9B%AE%E5%BD%95" tabindex="-1">📋 目录</h2><ol><li><a href="#%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88">架构概览</a></li><li><a href="#%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%9B%9B%E9%98%B6%E6%AE%B5%E8%AF%A6%E8%A7%A3">流水线四阶段详解</a></li><li><a href="#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E7%9A%84-playwright-%E6%8A%93%E5%8F%96">配置驱动的 Playwright 抓取</a></li><li><a href="#%E6%9C%AC%E5%9C%B0%E6%A8%A1%E5%9E%8B%E9%9B%86%E6%88%90">本地模型集成</a></li><li><a href="#%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E6%88%90">向量数据库集成</a></li><li><a href="#%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0">核心代码实现</a></li><li><a href="#%E9%83%A8%E7%BD%B2%E4%B8%8E%E8%BF%90%E7%BB%B4">部署与运维</a></li></ol><hr /><h2 id="%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88" tabindex="-1">架构概览</h2><h3 id="%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E5%9B%BE" tabindex="-1">系统架构图</h3><pre><code class="language-">┌─────────────────────────────────────────────────────────────────────────────┐│                           RSS AI Pipeline                                    │├─────────────────────────────────────────────────────────────────────────────┤│                                                                              ││  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────────────────┐   ││  │  RSS 源  │───▶│Gatekeeper│───▶│ DeepRead │───▶│  Data Enrichment    │   ││  │  (多源)  │     │  (筛选)  │    │ (抓取)   │    │  (评分+向量化)      │   ││  └──────────┘    └──────────┘    └──────────┘    └──────────────────────┘   ││                        │              │                     │                ││                        ▼              ▼                     ▼                ││                  ┌──────────────────────────────────────────────┐            ││                  │            MongoDB (FeedItem_YYYYMM)         │            ││                  │   process_status: 0 → 10 → 20 → 100/-1/99    │            ││                  └──────────────────────────────────────────────┘            ││                                                                  │            ││                                                                  ▼            ││                                                          ┌──────────┐        ││                                                          │  Qdrant  │        ││                                                          │ (向量库) │        ││                                                          └──────────┘        │└─────────────────────────────────────────────────────────────────────────────┘</code></pre><h3 id="%E6%8A%80%E6%9C%AF%E6%A0%88" tabindex="-1">技术栈</h3><table><thead><tr><th>层级</th><th>技术选型</th><th>说明</th></tr></thead><tbody><tr><td><strong>后端</strong></td><td>.NET 8 + <a href="http://ASP.NET" target="_blank">ASP.NET</a> Core</td><td>BackgroundService 驱动的异步流水线</td></tr><tr><td><strong>前端</strong></td><td>Vue 3 + Vite</td><td>管理后台和监控面板</td></tr><tr><td><strong>数据库</strong></td><td>MongoDB</td><td>按月分表存储 FeedItem</td></tr><tr><td><strong>向量库</strong></td><td>Qdrant</td><td>高性能向量搜索引擎</td></tr><tr><td><strong>AI 评分</strong></td><td>Gemini / Qwen (Ollama)</td><td>内容分析和评分</td></tr><tr><td><strong>AI 向量</strong></td><td>BGE-M3 (Ollama)</td><td>中英文混合 Embedding</td></tr><tr><td><strong>浏览器自动化</strong></td><td>Playwright</td><td>处理需要登录的站点</td></tr></tbody></table><h3 id="%E7%8A%B6%E6%80%81%E6%B5%81%E8%BD%AC" tabindex="-1">状态流转</h3><pre><code class="language-">ProcessStatus 状态机：    ┌─────────────────────────────────────────────────────────┐    │                                                         │    ▼                                                         │[0: PendingTriage] ──Gatekeeper──▶ [10: PendingDeepRead]     │         │                                  │                 │         │ (无规则/不匹配)                   │                 │         ▼                                  ▼                 │  [-1: Ignored]              [20: PendingAnalysis]           │                                           │                  │                              ┌────────────┴────────────┐     │                              ▼                         ▼     │                     [99: LowQuality]          [100: Done] ◀──┘                     (评分&lt;60/无正文)           (写入Qdrant)</code></pre><hr /><h2 id="%E6%B5%81%E6%B0%B4%E7%BA%BF%E5%9B%9B%E9%98%B6%E6%AE%B5%E8%AF%A6%E8%A7%A3" tabindex="-1">流水线四阶段详解</h2><h3 id="%E9%98%B6%E6%AE%B5-1%3A-gatekeeper%EF%BC%88%E5%AE%88%E9%97%A8%E5%91%98%EF%BC%89" tabindex="-1">阶段 1: Gatekeeper（守门员）</h3><p><strong>职责</strong>：基于白名单规则过滤低价值内容，只有明确匹配的内容才能进入下一阶段。</p><p><strong>核心逻辑</strong>：</p><pre><code class="language-csharp">// 严格白名单模式：必须配置 Keywords 且命中才放行if (rules.Keywords == null || rules.Keywords.Count == 0){    item.ProcessStatus = FeedStatus.Ignored; // 无规则 = 拒绝    return;}bool matchedKeyword = rules.Keywords.Any(k =&gt;     title.Contains(k, StringComparison.OrdinalIgnoreCase));if (!matchedKeyword){    item.ProcessStatus = FeedStatus.Ignored; // 不匹配 = 拒绝    return;}// 检查黑名单和最小长度...item.ProcessStatus = FeedStatus.PendingDeepRead; // 10</code></pre><p><strong>配置示例</strong> (<code>RssNode.ValidationRules</code>):</p><pre><code class="language-json">{    &quot;Fetcher&quot;: &quot;Playwright&quot;,    &quot;Keywords&quot;: [&quot;AI&quot;, &quot;深度学习&quot;, &quot;GPT&quot;, &quot;大模型&quot;],    &quot;Blockwords&quot;: [&quot;广告&quot;, &quot;推广&quot;],    &quot;MinTitleLength&quot;: 10}</code></pre><h3 id="%E9%98%B6%E6%AE%B5-2%3A-deepread%EF%BC%88%E6%B7%B1%E5%BA%A6%E6%8A%93%E5%8F%96%EF%BC%89" tabindex="-1">阶段 2: DeepRead（深度抓取）</h3><p><strong>职责</strong>：获取文章完整正文，支持多种抓取策略。</p><p><strong>策略模式架构</strong>：</p><pre><code class="language-csharp">public interface IContentFetcher{    string StrategyName { get; }    bool CanHandle(RssNode node);    Task&lt;string&gt; FetchContentAsync(FeedItem item, RssNode node);}// 策略实现- DefaultContentFetcher: HTTP + HtmlAgilityPack- PlaywrightContentFetcher: 浏览器自动化（需登录站点）</code></pre><p><strong>智能正文提取</strong>（移除网页噪音）：</p><pre><code class="language-javascript">// 移除导航、侧边栏、广告等const noiseSelectors = [    &#39;nav&#39;, &#39;footer&#39;, &#39;header&#39;, &#39;aside&#39;,    &#39;.sidebar&#39;, &#39;.ads&#39;, &#39;.comment&#39;, &#39;.share&#39;,    &#39;[class*=&quot;navigation&quot;]&#39;, &#39;[id*=&quot;footer&quot;]&#39;];noiseSelectors.forEach(sel =&gt; document.querySelectorAll(sel).forEach(e =&gt; e.remove()));// 优先查找正文容器const articleSelectors = [&#39;article&#39;, &#39;main&#39;, &#39;.article-content&#39;, &#39;.post-body&#39;];for (const sel of articleSelectors) {    const el = document.querySelector(sel);    if (el &amp;&amp; el.innerText.trim().length &gt; 200) return el.innerText;}return document.body.innerText;</code></pre><h3 id="%E9%98%B6%E6%AE%B5-3%3A-dataenrichment%EF%BC%88%E6%95%B0%E6%8D%AE%E5%A2%9E%E5%BC%BA%EF%BC%89" tabindex="-1">阶段 3: DataEnrichment（数据增强）</h3><p><strong>职责</strong>：AI 评分 + 向量化 + 写入 Qdrant。</p><p><strong>评分逻辑</strong>：</p><pre><code class="language-csharp">// 调用 LLM 分析内容var analysis = await scoringService.AnalyzeItemAsync(item);// analysis = { Complexity: 1-5, Sentiment: &quot;Positive/Neutral/Negative&quot;, Keywords: [...] }// 计算评分item.Score = analysis.Complexity * 20.0;if (analysis.Sentiment == &quot;Negative&quot;) item.Score -= 10;if (analysis.Sentiment == &quot;Positive&quot;) item.Score += 5;// 质量门控if (item.Score &lt; 60 || content.StartsWith(&quot;[DeepRead Fallback]&quot;)){    item.ProcessStatus = 99; // LowQuality，不写 Qdrant    return;}</code></pre><p><strong>向量化流程</strong>：</p><pre><code class="language-csharp">// 1. 生成 Embeddingfloat[] vector = await embedder.GenerateEmbeddingAsync(text);// 2. 写入 Qdrantawait vectorStore.UpsertAsync(pointId, vector, new Dictionary&lt;string, object&gt;{    { &quot;mongo_id&quot;, item._id.ToString() },    { &quot;title&quot;, item.Title },    { &quot;site_name&quot;, node.SiteName },    { &quot;score&quot;, item.Score }});item.ProcessStatus = 100; // Done</code></pre><h3 id="%E9%98%B6%E6%AE%B5-4%3A-isaienabled-%E5%89%8D%E7%BD%AE%E6%A3%80%E6%9F%A5" tabindex="-1">阶段 4: IsAiEnabled 前置检查</h3><p><strong>所有阶段共享的 AI 开关检查</strong>：</p><pre><code class="language-csharp">// 只查询 AI 已启用的节点的数据var aiEnabledNodeIds = await nodeCollection    .Find(n =&gt; n.IsAiEnabled == 1)    .Project(n =&gt; n.Id)    .ToListAsync();var filter = Builders&lt;FeedItem&gt;.Filter.And(    Builders&lt;FeedItem&gt;.Filter.Eq(x =&gt; x.ProcessStatus, targetStatus),    Builders&lt;FeedItem&gt;.Filter.In(x =&gt; x.RssNodeId, aiEnabledNodeIds));</code></pre><p>这确保了只有明确启用 AI 的节点，其文章才会进入流水线。</p><hr /><h2 id="%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E7%9A%84-playwright-%E6%8A%93%E5%8F%96" tabindex="-1">配置驱动的 Playwright 抓取</h2><h3 id="%E9%9B%B6%E9%85%8D%E7%BD%AE%E9%83%A8%E7%BD%B2%E7%AD%96%E7%95%A5" tabindex="-1">零配置部署策略</h3><p>系统支持&quot;配置即文件&quot;的部署模式：</p><pre><code class="language-">cookies/├── linux.do.json         # Playwright storageState (登录态)├── linux.do.plrule.json  # Playwright 配置 (代理、超时等)├── blog.csdn.net.json└── blog.csdn.net.plrule.json</code></pre><p><strong>自动发现机制</strong>：</p><pre><code class="language-csharp">public bool CanHandle(RssNode node){    // 1. 检查数据库配置    if (!string.IsNullOrEmpty(node.ValidationRules))    {        var doc = JsonDocument.Parse(node.ValidationRules);        if (doc.RootElement.TryGetProperty(&quot;Fetcher&quot;, out var val))            if (val.GetString() == &quot;Playwright&quot;) return true;    }        // 2. 检查磁盘文件（零配置模式）    var host = new Uri(node.SiteUrl).Host;    var path = Path.Combine(AppContext.BaseDirectory, &quot;cookies&quot;, $&quot;{host}.plrule.json&quot;);    if (File.Exists(path)) return true;        return false;}</code></pre><p><strong>首次配置流程</strong>：</p><ol><li>开发环境调用 Controller API（如 <code>/api/linuxdo/latest</code>）</li><li>Playwright 弹出浏览器，手动登录</li><li>登录成功后自动保存 <code>storageState</code> 和 <code>.plrule.json</code></li><li>将 <code>cookies/</code> 目录复制到生产环境</li><li>后台服务自动使用保存的配置</li></ol><p><strong>FetcherOptions 配置</strong>：</p><pre><code class="language-csharp">public class FetcherOptions{    public string TargetUrl { get; set; }    public string Proxy { get; set; }    public string CookieFileName { get; set; }    public bool InteractiveLogin { get; set; } = false;    public string LoginIndicatorUrl { get; set; }    public string LoginIndicatorTitle { get; set; }    public int NavigationTimeoutMs { get; set; } = 30000;}</code></pre><hr /><h2 id="%E6%9C%AC%E5%9C%B0%E6%A8%A1%E5%9E%8B%E9%9B%86%E6%88%90" tabindex="-1">本地模型集成</h2><h3 id="%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1" tabindex="-1">架构设计</h3><p>支持云端 (Gemini) 和本地 (Ollama) 双模式，通过配置切换：</p><pre><code class="language-json">{    &quot;LocalModelSettings&quot;: {        &quot;EnableEmbedding&quot;: true,        &quot;EnableChat&quot;: true,        &quot;BaseUrl&quot;: &quot;http://192.168.2.240:11434/v1&quot;,        &quot;EmbeddingModel&quot;: &quot;bge-m3&quot;,        &quot;ChatModel&quot;: &quot;qwen2.5:7b&quot;    },    &quot;GoogleAISettings&quot;: {        &quot;ApiKey&quot;: &quot;your-api-key&quot;,        &quot;ModelId&quot;: &quot;gemini-2.0-flash&quot;    }}</code></pre><h3 id="embedding-%E6%9C%8D%E5%8A%A1%E5%AE%9E%E7%8E%B0" tabindex="-1">Embedding 服务实现</h3><pre><code class="language-csharp">public async Task&lt;float[]&gt; GenerateEmbeddingAsync(string text){    var localSection = _config.GetSection(&quot;LocalModelSettings&quot;);    if (localSection.Exists() &amp;&amp; localSection.GetValue&lt;bool&gt;(&quot;EnableEmbedding&quot;))    {        // 使用本地 Ollama        var baseUrl = localSection[&quot;BaseUrl&quot;] ?? &quot;http://localhost:11434/v1&quot;;        var model = localSection[&quot;EmbeddingModel&quot;] ?? &quot;bge-m3&quot;;        return await CallLocalEmbeddingAsync(baseUrl, model, text);    }        // 回退到 Gemini    return await CallGeminiEmbeddingAsync(text);}private async Task&lt;float[]&gt; CallLocalEmbeddingAsync(string baseUrl, string model, string text){    var url = baseUrl.TrimEnd(&#39;/&#39;) + &quot;/embeddings&quot;;    var payload = new { model = model, input = text };    // ... 调用 Ollama OpenAI 兼容 API}</code></pre><h3 id="%E6%A8%A1%E5%9E%8B%E9%80%89%E6%8B%A9%E5%BB%BA%E8%AE%AE" tabindex="-1">模型选择建议</h3><table><thead><tr><th>用途</th><th>推荐模型</th><th>内存需求</th><th>说明</th></tr></thead><tbody><tr><td><strong>Embedding</strong></td><td>BGE-M3</td><td>4-6GB</td><td>中英文混合最佳</td></tr><tr><td><strong>评分分析</strong></td><td>Qwen2.5:7b</td><td>5-6GB</td><td>中文理解能力强</td></tr><tr><td><strong>轻量部署</strong></td><td>nomic-embed + phi3</td><td>3-4GB</td><td>资源受限场景</td></tr></tbody></table><hr /><h2 id="%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E6%88%90" tabindex="-1">向量数据库集成</h2><h3 id="qdrant-collection-%E8%AE%BE%E8%AE%A1" tabindex="-1">Qdrant Collection 设计</h3><pre><code class="language-bash"># 创建 Collectioncurl -X PUT &quot;http://localhost:6333/collections/rss_embeddings&quot; \  -H &quot;Content-Type: application/json&quot; \  -d &#39;{    &quot;vectors&quot;: {        &quot;size&quot;: 1024,        &quot;distance&quot;: &quot;Cosine&quot;    }}&#39;</code></pre><h3 id="point-%E7%BB%93%E6%9E%84" tabindex="-1">Point 结构</h3><pre><code class="language-json">{    &quot;id&quot;: &quot;uuid-from-mongo-objectid&quot;,    &quot;vector&quot;: [0.1, 0.2, ...],    &quot;payload&quot;: {        &quot;mongo_id&quot;: &quot;6957283c43445c014d93c5fe&quot;,        &quot;title&quot;: &quot;文章标题&quot;,        &quot;site_name&quot;: &quot;CSDN&quot;,        &quot;rss_node_id&quot;: 44,        &quot;score&quot;: 75,        &quot;pub_date&quot;: &quot;2026-01-02&quot;    }}</code></pre><h3 id="%E8%AF%AD%E4%B9%89%E6%90%9C%E7%B4%A2-api" tabindex="-1">语义搜索 API</h3><pre><code class="language-csharp">public async Task&lt;List&lt;SearchResult&gt;&gt; SearchAsync(string query, int topK = 10){    // 1. 生成查询向量    var queryVector = await _embedder.GenerateEmbeddingAsync(query);        // 2. 调用 Qdrant 搜索    var response = await _httpClient.PostAsJsonAsync(        $&quot;{_qdrantUrl}/collections/rss_embeddings/points/search&quot;,        new {            vector = queryVector,            top = topK,            with_payload = true,            filter = new {                must = new[] {                    new { key = &quot;score&quot;, range = new { gte = 60 } }                }            }        });        return await response.Content.ReadFromJsonAsync&lt;List&lt;SearchResult&gt;&gt;();}</code></pre><hr /><h2 id="%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0" tabindex="-1">核心代码实现</h2><h3 id="backgroundservice-%E6%A8%A1%E6%9D%BF" tabindex="-1">BackgroundService 模板</h3><pre><code class="language-csharp">public class DeepReadService : BackgroundService{    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        _logger.LogInformation(&quot;DeepRead Service Started&quot;);                while (!stoppingToken.IsCancellationRequested)        {            try            {                using var scope = _serviceProvider.CreateScope();                var factory = scope.ServiceProvider.GetRequiredService&lt;ContentFetcherFactory&gt;();                                // 获取 AI 已启用节点的待处理数据                var items = await GetPendingItemsAsync(stoppingToken);                                foreach (var item in items)                {                    await ProcessItemAsync(item, factory);                    await Task.Delay(1000); // 流控                }            }            catch (Exception ex)            {                _logger.LogError(ex, &quot;DeepRead Loop Error&quot;);                await Task.Delay(10000, stoppingToken);            }        }    }}</code></pre><h3 id="%E7%AD%96%E7%95%A5%E5%B7%A5%E5%8E%82" tabindex="-1">策略工厂</h3><pre><code class="language-csharp">public class ContentFetcherFactory{    private readonly IEnumerable&lt;IContentFetcher&gt; _fetchers;    public IContentFetcher GetFetcher(RssNode node)    {        // 优先专用策略        var specific = _fetchers.FirstOrDefault(f =&gt;             f.StrategyName != &quot;Default&quot; &amp;&amp; f.CanHandle(node));        if (specific != null) return specific;        // 回退默认 HTTP        return _fetchers.FirstOrDefault(f =&gt; f.StrategyName == &quot;Default&quot;);    }}</code></pre><h3 id="di-%E6%B3%A8%E5%86%8C" tabindex="-1">DI 注册</h3><pre><code class="language-csharp">// Program.csbuilder.Services.AddTransient&lt;IContentFetcher, DefaultContentFetcher&gt;();builder.Services.AddTransient&lt;IContentFetcher, PlaywrightContentFetcher&gt;();builder.Services.AddTransient&lt;ContentFetcherFactory&gt;();builder.Services.AddHostedService&lt;GatekeeperService&gt;();builder.Services.AddHostedService&lt;DeepReadService&gt;();builder.Services.AddHostedService&lt;DataEnrichmentService&gt;();</code></pre><hr /><h2 id="%E9%83%A8%E7%BD%B2%E4%B8%8E%E8%BF%90%E7%BB%B4" tabindex="-1">部署与运维</h2><h3 id="docker-compose-%E7%A4%BA%E4%BE%8B" tabindex="-1">Docker Compose 示例</h3><pre><code class="language-yaml">version: &#39;3.8&#39;services:  rss-adapter:    image: rss-adapter:latest    ports:      - &quot;5216:80&quot;    volumes:      - ./cookies:/app/cookies      - ./appsettings.json:/app/appsettings.json    depends_on:      - mongodb      - qdrant  mongodb:    image: mongo:6    volumes:      - mongo_data:/data/db  qdrant:    image: qdrant/qdrant    ports:      - &quot;6333:6333&quot;    volumes:      - qdrant_data:/qdrant/storage  ollama:    image: ollama/ollama    ports:      - &quot;11434:11434&quot;    volumes:      - ollama_data:/root/.ollama    deploy:      resources:        reservations:          devices:            - driver: nvidia              count: 1              capabilities: [gpu]</code></pre><h3 id="%E7%9B%91%E6%8E%A7%E6%8C%87%E6%A0%87" tabindex="-1">监控指标</h3><table><thead><tr><th>指标</th><th>来源</th><th>监控点</th></tr></thead><tbody><tr><td>处理吞吐量</td><td>ProcessStatus 分布</td><td>各状态数量变化</td></tr><tr><td>AI 调用次数</td><td>日志统计</td><td>成本控制</td></tr><tr><td>Qdrant 存储量</td><td>Qdrant API</td><td>向量数据增长</td></tr><tr><td>抓取成功率</td><td>DeepRead 日志</td><td>Fallback 比例</td></tr></tbody></table><h3 id="%E8%BF%90%E7%BB%B4%E5%BB%BA%E8%AE%AE" tabindex="-1">运维建议</h3><ol><li><strong>定期清理</strong>：删除 30 天前的低质量数据</li><li><strong>配置备份</strong>：<code>cookies/</code> 目录需纳入备份</li><li><strong>模型更新</strong>：定期 <code>ollama pull</code> 获取模型更新</li><li><strong>日志轮转</strong>：配置 Serilog 日志归档</li></ol><hr /><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>本文介绍的 RSS AI Pipeline 实现了：</p><ul><li>✅ <strong>多源聚合</strong>：支持任意 RSS 源，包括需要登录的站点</li><li>✅ <strong>智能过滤</strong>：基于规则的白名单/黑名单筛选</li><li>✅ <strong>深度抓取</strong>：Playwright + HtmlAgilityPack 双模式</li><li>✅ <strong>AI 增强</strong>：本地/云端模型灵活切换</li><li>✅ <strong>语义搜索</strong>：Qdrant 向量数据库支持</li></ul><p><strong>关键设计原则</strong>：</p><ul><li>配置驱动，避免硬编码</li><li>策略模式，易于扩展</li><li>Fail-Forward，保证流水线稳定</li><li>状态机管理，可追溯可重试</li></ul><hr /><p><em>本文基于 <a href="http://r.maifeipin.com" target="_blank">r.maifeipin.com</a> 项目已实现功能，由gemini 整理</em><br /><img src="/upload/2026/01/image.png" alt="image" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从“修路由器”到“重塑教育”：一场关于 AI 价值与知识阶梯的深度对话]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/cong--xiu-lu-you-qi--dao--zhong-su-jiao-yu--yi-chang-guan-yu-ai-jia-zhi-yu-zhi-shi-jie-ti-de-shen-du-dui-hua" />
                <id>tag:https://maifeipin.com,2025-12-27:cong--xiu-lu-you-qi--dao--zhong-su-jiao-yu--yi-chang-guan-yu-ai-jia-zhi-yu-zhi-shi-jie-ti-de-shen-du-dui-hua</id>
                <published>2025-12-27T09:17:11+08:00</published>
                <updated>2025-12-27T09:27:57+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p><strong>前言</strong>：<br />这是一次始于 Oracle VPS 网络故障排查，终于哲学思考的对话。<br />整个过程从一个不起眼的命令 <code>tracepath</code> 开始，延伸到“旁路由”这个民间词汇的学术定义，最终引爆了关于 AI 训练数据质量、知识阶梯以及未来 AI 教育形态的深度探讨。</p></blockquote><h2 id="%E4%B8%80%E3%80%81-%E5%BC%95%E5%AD%90%EF%BC%9A%E8%A2%AB%E5%BF%BD%E8%A7%86%E7%9A%84-tracepath-%E4%B8%8E%E8%AE%A4%E7%9F%A5%E7%9A%84%E7%9B%B2%E5%8C%BA" tabindex="-1">一、 引子：被忽视的 <code>tracepath</code> 与认知的盲区</h2><p>故事的起因很简单：我的 VPS 连不上外网了。</p><p>通常遇到这种情况，大家的第一反应是装 <code>traceroute</code> 去查路由。但问题是，现在的云服务器镜像（尤其是 Oracle 的）越来越精简，根本不预装 <code>traceroute</code>。</p><p><strong>我</strong>：</p><blockquote><p>“没网怎么装 traceroute？这也太死循环了。”</p></blockquote><p><strong>AI</strong>：</p><blockquote><p>“试试 <code>tracepath</code>。这是系统自带的，几乎所有 Linux 发行版都有，但很多人不知道。”</p></blockquote><p>这让我大为震惊。我作为一个老网民，一直以为 <code>traceroute</code> 是唯一解，甚至不知道 <code>ping</code> 其实也可以通过指定 TTL（生存时间）来模拟路由追踪（Windows 下是 <code>ping -i</code>，Linux 下是 <code>ping -t</code>）。</p><p><strong>这个发现补全了我排查 VPS 网络故障的最后一块拼图</strong>：其实只要结合 <code>ip route</code>（查路由表）、<code>iptables</code>（查防火墙）以及 <code>tracepath/ping TTL</code>（查实际跳数），就能精准地判断出数据包到底死在了哪一环：<strong>是连本机的网卡都没出（本地防火墙问题）？还是到了内网网关就被丢弃（云服务商配置问题）？亦或是冲出了内网却死在了Oracle VNC 路由安全规则上？</strong></p><p>正是这种从“无知”到“顿悟”的瞬间，引发了我对 AI 知识库的思考。如果 AI 只是机械地把所有技术文档背下来，它能在我无助的时候，精准地递给我这把叫 <code>tracepath</code> 的钥匙吗？</p><hr /><h2 id="%E4%BA%8C%E3%80%81-%E6%A0%B8%E5%BF%83%E5%8F%91%E9%97%AE%EF%BC%9A%E4%BD%8E%E9%98%B6%E6%95%B0%E6%8D%AE%E4%BC%9A%E7%A8%80%E9%87%8A-ai-%E7%9A%84%E6%99%BA%E6%85%A7%E5%90%97%EF%BC%9F" tabindex="-1">二、 核心发问：低阶数据会稀释 AI 的智慧吗？</h2><p>我意识到，AI 之所以能教我用土办法（Ping TTL）和冷门命令（tracepath），是因为它看过大量论坛里的“笨办法”和“小白贴”。</p><p><strong>我</strong>：</p><blockquote><p>“AI 的价值是打破信息壁垒，一般都是低阶的向高阶的提问来体现。<br />互联网上充斥着海量的低质量、不严谨的‘低阶数据’（比如各种错误的教程、水贴）。如果 AI 训练时灌入大量这类内容，会不会稀释它的智慧，影响它的价值？”</p></blockquote><p><strong>AI</strong>：<br />这是一个非常深刻的问题，直击大模型训练的痛点——<strong>GIGO (Garbage In, Garbage Out)</strong>。<br />如果 AI 不加筛选地全盘吸收，确实会出现 <strong>“劣币驱逐良币”</strong> 的效应，甚至导致 <strong>“模型崩溃” (Model Collapse)</strong> —— 变得越来越平庸，满嘴跑火车。</p><p>但顶级的 AI 团队并非简单地“喂数据”，核心壁垒在于 <strong>“数据清洗与配方 (Data Curation &amp; Recipe)”</strong>，这可能比模型架构本身更重要：</p><ol><li><p><strong>高权重 (High-Weight) —— 学习“脑子”</strong>：</p><ul><li><strong>来源</strong>：学术论文 (arXiv)、高质量开源代码 (Linux Kernel)、StackOverflow 高分回答、维基百科。</li><li><strong>作用</strong>：这些数据逻辑严密、因果清晰。AI 通过反复训练这些数据，学会的是 <strong>推理能力 (Reasoning)</strong> 和 <strong>严谨的逻辑链</strong>。</li></ul></li><li><p><strong>低权重 (Low-Weight) —— 学习“嘴巴”</strong>：</p><ul><li><strong>来源</strong>：Reddit 吐槽、贴吧灌水、日常对话、非严谨的技术博客。</li><li><strong>作用</strong>：这些数据虽然逻辑可能松散，甚至包含错误，但它们包含了极具价值的 <strong>人类表达习惯</strong> 和 <strong>丰富的应用场景 (Context)</strong>。AI 用它们来学习怎么“说人话”，怎么理解“小白的痛点”。</li></ul></li></ol><p><strong>关键结论</strong>：<br />只有高阶数据，AI 会变成一个懂理论但听不懂人话的“老学究”；<br />只有低阶数据，AI 会变成一个只会玩梗但解决不了问题的“盲流”。<br /><strong>价值恰恰产生于连接</strong>：利用海量的低阶数据提供丰富的<strong>落地土壤</strong>，让高阶的底层逻辑得以生根发芽。哪怕是错误的数据，也有价值——它教会了 AI <strong>“人类通常会在哪里犯错”</strong>。</p><hr /><h2 id="%E4%B8%89%E3%80%81-%E9%9A%90%E5%96%BB%EF%BC%9A%E2%80%9C%E6%97%81%E8%B7%AF%E7%94%B1%E2%80%9D%E4%B8%8E%E5%B7%B2%E6%89%93%E7%A0%B4%E7%9A%84%E5%A3%81%E5%9E%92" tabindex="-1">三、 隐喻：“旁路由”与已打破的壁垒</h2><p>为了验证刚才学到的 <code>ping -i</code>，我在 Windows 下试了一下，并把前 3 跳的结果复制给了 AI。AI 很快发现了我家网络的特殊结构：根据前几跳 IP 在同一网段徘徊的特征，推断出我用了“旁路由”。</p><p><strong>我</strong>：</p><blockquote><p>“这都被你看出来了？这玩意儿叫‘旁路由’，在你们专业视角看，路由表是不是有点乱？”</p></blockquote><p><strong>AI</strong>：</p><blockquote><p>“哈哈哈，一眼就看出来的。不过‘旁路由’在正统计算机网络教材里是查不到的。学术上它叫 <strong>‘默认网关重定向’ (Default Gateway Redirection)</strong> 或 <strong>‘单臂路由’</strong>。但咱们聊天，用‘旁路由’最接地气，一听就懂。”</p></blockquote><p><strong>AI 的价值在于</strong>：<br />它能瞬间理解“旁路由”这个<strong>民间土话 (低阶)</strong>，并在后台将其映射到严谨的 <strong>ARP协议与网关指向 (高阶)</strong> 来分析问题。<br /><strong>这就是打破信息壁垒的本质：用高阶的底层逻辑，去解释低阶的表象问题。</strong></p><hr /><h2 id="%E5%9B%9B%E3%80%81-%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B%EF%BC%9Aai-%E5%A6%82%E4%BD%95%E9%A2%A0%E8%A6%86%E4%BC%A0%E7%BB%9F%E6%95%99%E8%82%B2" tabindex="-1">四、 未来展望：AI 如何颠覆传统教育</h2><p><strong>我</strong>：</p><blockquote><p>“这套方法论成熟以后，是不是就可以改变现有的传统的教学方式了？”</p></blockquote><p><strong>AI</strong>：<br />这预示着教育从 <strong>“工业化标准化”</strong> 到 <strong>“极致个性化”</strong> 的革命。</p><h3 id="1.-%E8%A7%A3%E5%86%B3%E2%80%9C%E6%8E%A5%E5%8F%A3%E4%B8%8D%E5%8C%B9%E9%85%8D%E2%80%9D" tabindex="-1">1. 解决“接口不匹配”</h3><ul><li><strong>传统教育</strong>：老师输出固定频率的信号（统一教材），学生接收不到就只能掉队。</li><li><strong>AI 教育</strong>：<strong>动态降维</strong>。<ul><li>对喜欢赛车的学生，AI 会用“引擎转速”来解释微积分的“导数”。</li><li>对喜欢做饭的学生，AI 会用“配料比例”来解释化学的“摩尔浓度”。</li><li>AI 完成了从“高阶严谨逻辑”到“低阶生活经验”的<strong>实时翻译</strong>。</li></ul></li></ul><h3 id="2.-%E6%B6%88%E9%99%A4%E2%80%9C%E7%9F%A5%E8%AF%86%E7%9A%84%E8%AF%85%E5%92%92%E2%80%9D-(curse-of-knowledge)" tabindex="-1">2. 消除“知识的诅咒” (Curse of Knowledge)</h3><p>专家（老师）往往很难理解初学者为什么“不懂”。因为 AI 见识过海量的“错误数据”和“蠢问题”，它理解 <strong>“愚蠢的形状”</strong>。<br />它知道你卡住是因为把 <code>ping -t</code> (TTL) 理所当然地当成了 Windows 下的无限制 Ping，而不是仅仅重复正确的定义。<br /><strong>只有理解“错”是怎么发生的，才能精准地教会“对”。</strong></p><h3 id="3.-%E6%95%99%E8%82%B2%E7%9B%AE%E6%A0%87%E7%9A%84%E8%BD%AC%E7%A7%BB" tabindex="-1">3. 教育目标的转移</h3><p>从 <strong>Memory (记忆)</strong> 转向 <strong>Reasoning (推理与验证)</strong>。<br />未来的学生不需要成为硬盘（存知识），而需要成为 CPU（处理知识）。<br />面对 AI 可能产生的幻觉，人类必须学会像今天使用 <code>tracepath</code> 一样，去 <strong>验证</strong> 答案。</p><hr /><h2 id="%E4%BA%94%E3%80%81-%E7%BB%93%E8%AF%AD%EF%BC%9A%E7%AB%99%E5%9C%A8%E6%8C%87%E6%95%B0%E7%BA%A7%E7%88%86%E5%8F%91%E7%9A%84%E8%8A%82%E7%82%B9" tabindex="-1">五、 结语：站在指数级爆发的节点</h2><p><strong>我</strong>：</p><blockquote><p>“可能地球上有一半的人都还没有领悟到……人类文明大发展后面这一小段时间，绝对是最快的。”</p></blockquote><p><strong>AI</strong>：<br />技术的发展是指数级的，而人的观念往往是线性的。<br />我们正在经历的，不仅仅是造出更聪明的大脑，更是 <strong>降低“使用智慧”的门槛</strong>。</p><p>让知识不再是象牙塔里的特权，而是像自来水一样，任何人拧开水龙头（提问），都能喝到最纯净的智慧。</p><p><em>(整理自与 谷歌 DeepMind Antigravity 的对话)</em></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[WebMusic GO ON  v2.7 ]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/webmusicgoon" />
                <id>tag:https://maifeipin.com,2025-12-21:webmusicgoon</id>
                <published>2025-12-21T17:17:44+08:00</published>
                <updated>2025-12-21T17:37:08+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p><strong>摘要</strong>：本文记录了 WebMusic 项目在一天内的架构演进过程。通过引入多租户数据隔离、RBAC 权限控制及审计日志中间件，项目从一个个人使用的音乐播放器升级为具备企业级安全特性的私有云服务。</p></blockquote><p>在 v2.7.0 版本的迭代中，我们主要关注系统的安全性、多用户隔离以及可维护性。以下是本次重构的核心技术点复盘。</p><h2 id="1.-%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84%E5%8D%87%E7%BA%A7%EF%BC%9A%E5%A4%9A%E7%A7%9F%E6%88%B7%E6%95%B0%E6%8D%AE%E9%9A%94%E7%A6%BB-(multi-tenancy)" tabindex="-1">1. 核心架构升级：多租户数据隔离 (Multi-Tenancy)</h2><p>为了支持演示账号（Demo User）和普通多用户场景，仅仅在前端隐藏入口是远远不够的。我们对后端核心控制器进行了基于 <code>UserId</code> 的物理级隔离改造。</p><ul><li><strong>挑战</strong>：如何确保 Admin 用户配置的敏感 SMB 存储凭据不被其他用户扫描或访问？</li><li><strong>方案</strong>：<ul><li>在 <code>ScanSource</code> 和 <code>MediaFile</code> 实体中引入 Ownership 概念。</li><li>重构 <code>MediaController</code> 的查询逻辑，从早期的 Navigation Property 过滤升级为更严谨的 <code>Where(m =&gt; allowedSourceIds.Contains(m.SourceId))</code>，杜绝了通过 ID 遍历可能导致的数据越权。</li></ul></li></ul><h2 id="2.-%E8%B5%84%E4%BA%A7%E9%A3%8E%E6%8E%A7%E4%B8%8E%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6-(rbac)" tabindex="-1">2. 资产风控与权限控制 (RBAC)</h2><p>随着项目集成了 Gemini AI 用于歌词生成和自动标签，API 调用成本成为需要考虑的因素。针对 Demo 账户甚至未来的普通订阅用户，必须严格限制其对高成本接口的访问。</p><p>我们实施了基于 <strong>JWT Claims</strong> 的角色权限控制（RBAC）：</p><ul><li><strong>Token 升级</strong>：在 <code>auth/login</code> 颁发 Token 时，根据用户身份注入 <code>ClaimTypes.Role</code> (如 “Admin” 或 “User”)。</li><li><strong>后端拦截</strong>：<code>TagsController</code> 和 <code>LyricsController</code> 全面通过 <code>[Authorize(Roles = &quot;Admin&quot;)]</code> 属性进行保护。这比传统的在代码中判断 <code>UserId == 1</code> 更具扩展性和维护性。</li><li><strong>前端适配</strong>：UI 层根据 <code>isAdmin</code> 状态自动禁用 AI 相关功能按钮，并提供 “Restricted Mode” 的视觉反馈，优化用户体验。</li></ul><h2 id="3.-%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7%EF%BC%9A%E5%AE%A1%E8%AE%A1%E6%97%A5%E5%BF%97%E4%B8%AD%E9%97%B4%E4%BB%B6-(audit-logging)" tabindex="-1">3. 可观测性：审计日志中间件 (Audit Logging)</h2><p>为了增强系统的可维护性和安全性，我们引入了自定义的审计机制。</p><ul><li><strong>中间件实现</strong>：开发了 <code>ApiLoggingMiddleware</code>，拦截关键 API 请求。</li><li><strong>记录维度</strong>：记录操作者身份（User）、请求路径、HTTP 方法、响应状态码及耗时。</li><li><strong>降噪策略</strong>：针对 <code>/stream</code> (流媒体分片) 和 <code>/cover</code> (封面图片) 等高频且低敏感度的请求进行了过滤，确保日志文件聚焦于核心业务操作（如数据修改、用户管理）。</li></ul><h2 id="4.-%E5%AE%8C%E6%95%B4%E7%9A%84%E7%94%A8%E6%88%B7%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%AE%A1%E7%90%86" tabindex="-1">4. 完整的用户生命周期管理</h2><p>为了闭环多用户功能，我们补全了用户管理模块。</p><ul><li>新增 <code>UsersController</code>，提供用户列表查询、创建用户、删除用户及管理员重置密码接口。</li><li>前端新增 <code>AdminPage</code>，提供可视化的用户管理面板。</li></ul><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>通过这次重构，WebMusic 不仅在功能上得到了补全，更在架构的健壮性上迈出了重要一步。从简单的 CRUD 到包含权限、审计、隔离的完整系统，这一过程验证了现代 Web 开发技术栈（.NET 8 + React）的高效性。</p><hr /><p><em>本文档基于项目 Git Commit Log (v2.6.x - v2.7.0) 自动生成，由 <strong>Gemini</strong> 辅助整理。</em></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[给WebMusic 增加 歌单分享 和AI TAG批量整理功能]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/gei-webmusic-zeng-jia-ge-dan-fen-xiang-he-aitag-pi-liang-zheng-li-gong-neng" />
                <id>tag:https://maifeipin.com,2025-12-14:gei-webmusic-zeng-jia-ge-dan-fen-xiang-he-aitag-pi-liang-zheng-li-gong-neng</id>
                <published>2025-12-14T02:15:20+08:00</published>
                <updated>2025-12-14T02:30:41+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="webmusic-%E6%96%B0%E5%8A%9F%E8%83%BD%E5%8F%91%E5%B8%83%EF%BC%9A%E6%AD%8C%E5%8D%95%E5%88%86%E4%BA%AB%E4%B8%8E-ai-%E6%A0%87%E7%AD%BE%E6%95%B4%E7%90%86" tabindex="-1">WebMusic 新功能发布：歌单分享与 AI 标签整理</h1><p>随着 WebMusic 的不断迭代，今天在这个版本中，我带来了两个非常实用的功能更新：<strong>安全的歌单分享</strong> 和 <strong>基于 GEMINI 的 AI 标签批量整理</strong>。</p><p>这两个功能解决了我长期以来的两个痛点：</p><ol><li>想把 NAS 里的好歌分享给朋友听，但又不想给他们 NAS 账号。</li><li>下载的歌曲元数据乱七八糟（文件名全是乱码或者 <code>Track 01.mp3</code>），手动整理太累。</li></ol><hr /><h2 id="%F0%9F%8E%B5-%E6%AD%8C%E5%8D%95%E5%88%86%E4%BA%AB%EF%BC%9A%E8%AE%A9%E9%9F%B3%E4%B9%90%E6%B5%81%E5%8A%A8%E8%B5%B7%E6%9D%A5" tabindex="-1">🎵 歌单分享：让音乐流动起来</h2><p>现在，你可以将任何歌单，或者歌单里选中的几首歌曲，一键生成分享链接发送给朋友。接收者无需登录，直接在浏览器中即可播放。</p><h3 id="%E6%A0%B8%E5%BF%83%E7%89%B9%E6%80%A7" tabindex="-1">核心特性</h3><ul><li><strong>灵活分享模式</strong>：<ul><li><strong>整单分享</strong>：直接分享现有歌单，朋友看到的歌单内容会随你的更新而变化。</li><li><strong>选曲分享</strong>：只选中几首特定的歌，系统会自动创建一个临时的&quot;分享列表&quot;，适合安利特定曲目。</li></ul></li><li><strong>安全控制</strong>：<ul><li><strong>密码保护</strong>：可以设置访问密码，防止链接泄露。</li><li><strong>有效期设置</strong>：支持设置链接的有效期（如 1 天、7 天），过期自动失效。</li></ul></li><li><strong>独立的播放体验</strong>：<ul><li>分享页面是一个独立的播放器应用 (<code>SharedPlaylistPage</code>)。</li><li><strong>断点续听</strong>：朋友听了一半关闭页面，下次打开会自动恢复到上次播放的位置（基于 LocalStorage）。</li><li><strong>锁屏控制</strong>：支持 Media Session API，在手机锁屏界面也能切歌。</li></ul></li></ul><h3 id="%E6%8A%80%E6%9C%AF%E5%AE%9E%E7%8E%B0" tabindex="-1">技术实现</h3><p>后端在 <code>PlaylistController</code> 中新增了 <code>SharePlaylist</code> 接口，生成唯一的 <code>ShareToken</code>。为了保证安全性，分享页面的接口 <code>GetSharedPlaylist</code> 使用 <code>[AllowAnonymous]</code> 允许匿名访问，但严格校验 Token、有效期和密码。</p><pre><code class="language-csharp">// 后端：生成分享链接[HttpPost(&quot;{id}/share&quot;)]public async Task&lt;IActionResult&gt; SharePlaylist(int id, [FromBody] SharePlaylistDto dto){    // ... 生成 Token，设置 Password 和 Expiry ...    return Ok(new { shareUrl = $&quot;/share/{token}&quot; });}</code></pre><p>前端实现了一个精简版的播放器，去除了所有管理功能，专注于&quot;听&quot;的体验。</p><p><img src="/upload/2025/12/image-1765650621799.png" alt="image-1765650621799" /></p><hr /><h2 id="%F0%9F%A4%96-ai-%E6%A0%87%E7%AD%BE%E6%95%B4%E7%90%86%EF%BC%9A%E5%91%8A%E5%88%AB-%E2%80%9Cunknown-artist%E2%80%9D" tabindex="-1">🤖 AI 标签整理：告别 “Unknown Artist”</h2><p>这是我最喜欢的功能。利用 Google 最新的 <strong>Gemini 2.0 Flash</strong> 模型，WebMusic 现在可以智能分析歌曲的文件名和路径，自动补充缺失的元数据。</p><h3 id="%E7%97%9B%E7%82%B9%E5%9C%BA%E6%99%AF" tabindex="-1">痛点场景</h3><p>你是否也有这样的文件：</p><ul><li><code>/Music/周杰伦/2004-七里香/02. 搁浅.mp3</code> -&gt; 标签里却是空的，播放器显示 “Unknown Title”</li><li><code>/Music/Eason/富士山下.mp3</code> -&gt; 只有文件名，没有专辑信息</li></ul><h3 id="ai-%E6%95%B4%E7%90%86%E6%B5%81%E7%A8%8B" tabindex="-1">AI 整理流程</h3><ol><li><p><strong>智能上下文提取</strong>：<br />后端不仅发送文件名，还会发送<strong>父文件夹名称</strong>给 AI。<br />例如对于 <code>/周杰伦/七里香/搁浅.mp3</code>，AI 会知道 “周杰伦” 是艺术家，“七里香” 是专辑。</p></li><li><p><strong>批量处理</strong>：<br />在 <code>TagsController</code> 中，我们实现了 <code>SuggestRequest</code>，支持一次性发送 50 首歌给 Gemini。</p></li><li><p><strong>Gemini 2.0 Flash 加持</strong>：<br />使用最新的 Flash 模型，速度极快且成本极低。AI 会返回标准的 JSON 格式，包含 <code>Title</code>, <code>Artist</code>, <code>Album</code>, <code>Genre</code>, <code>Year</code>。</p></li></ol><pre><code class="language-csharp">// 给 AI 的 Prompt 上下文var contextData = songs.Select(m =&gt; new {    FileName = fileName,    FolderName = parentFolder // 关键：利用文件夹结构辅助识别});</code></pre><h3 id="%E5%AE%9E%E9%99%85%E6%95%88%E6%9E%9C" tabindex="-1">实际效果</h3><p>实测下来，对于只有文件名的老歌，AI 的识别准确率惊人。它甚至能根据歌名自动补全<code>Genre</code>（流派）和 <code>Year</code>（年份），这是传统正则提取做不到的。</p><h2 id="" tabindex="-1"><img src="/upload/2025/12/image-1765648920633.png" alt="image-1765648920633" /></h2><h2 id="%E7%BB%93%E8%AF%AD" tabindex="-1">结语</h2><p>WebMusic 的初衷是管理好自己的本地音乐库。</p><ul><li><strong>AI 整理</strong> 帮我把库变得整洁有序。</li><li><strong>歌单分享</strong> 让我能把这份有序的美好分享给他人。</li></ul><p>Enjoy the music! 🎧</p><hr /><p><em>本文基于 <a href="https://github.com/maifeipin/WebMusic" target="_blank">WebMusic</a> v2 版本开发日志生成。</em></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[终于凑齐了，我的互联网三件套]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/zhong-yu-cou-qi-lao--wo-de-hu-lian-wang-san-jian-tao" />
                <id>tag:https://maifeipin.com,2025-12-12:zhong-yu-cou-qi-lao--wo-de-hu-lian-wang-san-jian-tao</id>
                <published>2025-12-12T08:30:51+08:00</published>
                <updated>2025-12-12T09:27:34+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>作为一个 80 后数字居民，我一直有个小小的执念：在这个互联网时代，我想拥有一片完全属于自己的数字领地。不是寄人篱下的微博、知乎，不是受制于算法的头条、抖音，而是真正由自己掌控的——<strong>写</strong>、<strong>读</strong>、<strong>听</strong>三件套。</p><p>今天，随着我的音乐个人站的上线，这个愿望终于实现了。</p><hr /><h2 id="%F0%9F%96%8A%EF%B8%8F-%E5%86%99%E8%87%AA%E5%B7%B1%E6%89%80%E6%83%B3%EF%BC%9Amaifeipin.com" tabindex="-1">🖊️ 写自己所想：<a href="https://maifeipin.com" target="_blank">maifeipin.com</a></h2><p><img src="/upload/2025/12/image-1765498963579.png" alt="image-1765498963579" /></p><p>博客是我最早拥有的一块自留地。基于 <a href="https://github.com/halo-dev/halo" target="_blank">Halo</a> 搭建，主题简洁优雅，取名「个人资料」—— 半生年华，三分浮萍，六分岩岩。</p><p>这里记录着我的技术探索与生活感悟：</p><ul><li><strong>AI 时代的新玩法</strong>：从 gemini-cli 实现 vibe code，到用 gemini-embedding 给云笔记做 RAG，打造智能个人知识库</li><li><strong>开发工具的折腾</strong>：Remote-SSH 唤醒远程桌面、Trae + task-master 自动化完成复杂项目任务</li><li><strong>80后的回忆</strong>：1980-1999 年的粤语金曲，那些青春里单曲循环的旋律</li><li><strong>AI 的情绪价值</strong>：原来 AI 不只是会写代码，还会哄人开心</li></ul><p>没有算法推荐，没有信息茧房，只有纯粹的文字。想写什么就写什么，想什么时候写就什么时候写。这种自由，是任何平台都给不了的。</p><hr /><h2 id="%F0%9F%93%96-%E7%9C%8B%E8%87%AA%E5%B7%B1%E6%89%80%E7%9C%8B%EF%BC%9Ar.maifeipin.com" tabindex="-1">📖 看自己所看：<a href="https://r.maifeipin.com" target="_blank">r.maifeipin.com</a></h2><p><img src="/upload/2025/12/image-1765499052982.png" alt="image-1765499052982" /><br />在信息爆炸的时代，我选择用最古老的方式获取信息——<strong>RSS</strong>。</p><p>「简阅 RSS」是我基于 Vue 3 + Bootstrap 5 搭建的私人 RSS 阅读器。界面简洁，功能纯粹：</p><ul><li><strong>订阅我想看的</strong>：技术博客、独立博主、优质 Newsletter</li><li><strong>过滤我不想看的</strong>：没有热搜、没有广告、没有&quot;猜你喜欢&quot;</li><li><strong>保持我的阅读节奏</strong>：早起一杯咖啡，晚间半小时阅读</li></ul><p>每天固定时间，打开我的 RSS，看看这个世界发生了什么值得关注的事。不被推送打扰，不被标题党欺骗，信息的主动权，握在自己手里。</p><p>RSS 已死？不，RSS 只是回归了它本来的样子——一个为真正需要它的人服务的工具。</p><hr /><h2 id="%F0%9F%8E%B5-%E5%90%AC%E8%87%AA%E5%B7%B1%E6%89%80%E5%90%AC%EF%BC%9Amusic.maifeipin.com" tabindex="-1">🎵 听自己所听：<a href="https://music.maifeipin.com" target="_blank">music.maifeipin.com</a></h2><p><img src="/upload/2025/12/image-1765499436715.png" alt="image-1765499436715" /><br /><img src="/upload/2025/12/image-1765499280715.png" alt="image-1765499280715" /></p><p>这是三件套中最后完成的一件，也是我唯一全AI制作的项目——<strong>WebMusic</strong>。</p><h3 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E8%87%AA%E5%B7%B1%E5%81%9A%E9%9F%B3%E4%B9%90%E6%92%AD%E6%94%BE%E5%99%A8%EF%BC%9F" tabindex="-1">为什么要自己做音乐播放器？</h3><p>原因很简单：</p><ol><li><strong>版权问题</strong>：曾经网易云和 QQ 音乐上的歌单，因为版权问题，歌曲越来越少</li><li><strong>会员陷阱</strong>：各平台的会员体系越来越复杂，今天还能听的歌，明天可能就要加钱了</li><li><strong>NAS 里的宝藏</strong>：我的 NAS 里躺着几TB 的无损音乐，却没有一个好用的方式去听它们</li></ol><h3 id="webmusic-%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" tabindex="-1">WebMusic 是什么？</h3><p>一个现代化的网页音乐播放器，专为 NAS 和 SMB 共享设计：</p><ul><li><strong>技术栈</strong>：.NET 8 后端 + React + Vite 前端 + Tailwind CSS</li><li><strong>SMB 集成</strong>：直接连接 Windows 共享 / SMB 服务器，扫描和流式播放音乐</li><li><strong>后台扫描</strong>：异步扫描管道，实时状态更新，轻松处理大型音乐库</li><li><strong>智能去重</strong>：跨多个共享防止重复的库条目</li><li><strong>自动转码</strong>：通过 FFmpeg 为不支持的格式（FLAC/ALAC → MP3/AAC）自动转码</li><li><strong>现代界面</strong>：响应式布局，最近播放、收藏夹、库统计一应俱全</li><li><strong>多视图浏览</strong>：扁平视图、分组视图（按艺术家/专辑/流派/年份）、目录视图</li></ul><h3 id="60-%E5%88%86%E9%92%9F%E4%B8%8A%E7%BA%BF%E4%B8%80%E4%B8%AA%E9%9F%B3%E4%B9%90%E7%AB%99" tabindex="-1">60 分钟上线一个音乐站</h3><p>最让我兴奋的是，这个项目几乎是用 AI 辅助完成的。从需求梳理，到代码生成，到部署上线，Antigravity 帮我节省了大量时间。这也是为什么我在博客里写了《60分钟 用 Antigravity 全自动 快速上线一个音乐站》。</p><p>现在，我可以：</p><ul><li>🎧 在任何设备上听我 NAS 里的音乐</li><li>📱 手机、平板、电脑，随时随地</li><li>🎵 FLAC、ALAC、MP3，各种格式通吃</li><li>❤️ 收藏喜欢的歌曲，管理播放列表</li><li>🕐 查看播放历史，重温曾经的单曲循环</li></ul><hr /><h2 id="%E4%B8%89%E4%BB%B6%E5%A5%97%E7%9A%84%E6%84%8F%E4%B9%89" tabindex="-1">三件套的意义</h2><p>有人可能会问：折腾这些有什么意义？平台那么多，为什么非要自己搞？</p><p>我的回答是：</p><blockquote><p><strong>这不是为了与世隔绝，而是为了在喧嚣中保持自我。</strong></p></blockquote><ul><li>写博客，是为了<strong>沉淀思考</strong>，而不是迎合流量</li><li>用 RSS，是为了<strong>主动获取</strong>，而不是被动投喂</li><li>建音乐站，是为了<strong>真正拥有</strong>，而不是租赁使用</li></ul><p>当然，我依然会用微信、刷抖音、看 B 站。但我知道，在这些平台之外，我还有一片自己的天地。那里没有算法，没有广告，没有社交压力——只有我想要的内容，和我想要的节奏。</p><hr /><h2 id="%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E" tabindex="-1">写在最后</h2><p>2025 年 12 月 12 日，我的互联网三件套终于凑齐了。</p><table><thead><tr><th>工具</th><th>地址</th><th>功能</th></tr></thead><tbody><tr><td>博客</td><td><a href="https://maifeipin.com" target="_blank">maifeipin.com</a></td><td>写自己所想</td></tr><tr><td>RSS</td><td><a href="https://r.maifeipin.com" target="_blank">r.maifeipin.com</a></td><td>看自己所看</td></tr><tr><td>音乐</td><td><a href="https://music.maifeipin.com" target="_blank">music.maifeipin.com</a></td><td>听自己所听</td></tr></tbody></table><p>这不是终点，而是新的起点。</p><p>未来，或许还会有第四件套、第五件套……谁知道呢？折腾的乐趣，本就在于过程。</p><p>如果你也想拥有自己的三件套，欢迎联系我交流。毕竟——</p><p><strong>半生年华，三分浮萍，六分岩岩。</strong></p><hr /><p><em>本文由 <a href="https://developers.google.com/products/gemini" target="_blank">Antigravity</a> 撰写，发布于 <a href="https://maifeipin.com" target="_blank">maifeipin.com</a></em></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[    60分钟 用 Antigravity 全自动 快速上线一个音乐站]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/60-fen-zhong-yong-antigravity-quan-zi-dong-kuai-su-shang-xian-yi-ge-yin-le-zhan" />
                <id>tag:https://maifeipin.com,2025-12-07:60-fen-zhong-yong-antigravity-quan-zi-dong-kuai-su-shang-xian-yi-ge-yin-le-zhan</id>
                <published>2025-12-07T16:15:42+08:00</published>
                <updated>2025-12-07T21:14:51+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="webmusic-%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80" tabindex="-1">WebMusic <a href="https://github.com/maifeipin/WebMusic" target="_blank">  项目地址</a></h3><p>A modern, web-based music player and library manager designed for NAS (Network Attached Storage) and SMB shares. Built with .NET 8 (Backend) and React + Vite (Frontend).</p><h2 id="features" tabindex="-1">Features</h2><h3 id="core-integration" tabindex="-1">Core Integration</h3><ul><li><strong>SMB Integration</strong>: Directly connect to Windows Shares / SMB servers to scan and stream music.</li><li><strong>Background Scanning</strong>: Asynchronous scanning pipeline with real-time status updates, capable of handling large libraries without timeout.</li><li><strong>Deduplication</strong>: Intelligent handling of physical files to prevent duplicate library entries across multiple shares.</li></ul><h3 id="playback-%26-audio" tabindex="-1">Playback &amp; Audio</h3><ul><li><strong>Global Player</strong>: Persistent playback bar with minimize/maximize support, play queue, and improved seek controls.</li><li><strong>Transcoding</strong>: Automatic transcoding (via FFmpeg) for unsupported formats (FLAC/ALAC -&gt; MP3/AAC) with seeking support.</li><li><strong>Smart Queue</strong>: Add songs, folders, or entire groups to queue.</li></ul><h3 id="user-experience" tabindex="-1">User Experience</h3><ul><li><strong>Modern Dashboard</strong>: Auto-responsive layout featuring “Recently Played”, “Favorites”, and Library Stats.</li><li><strong>Directory Browser</strong>: Interactive file browser to easily select SMB shares and folders.</li><li><strong>Library Views</strong>:<ul><li><strong>Flat View</strong>: Sortable list of all songs with Path column.</li><li><strong>Group View</strong>: Browse by Artist, Album, Genre, or Year.</li><li><strong>Directory View</strong>: Navigate your physical folder structure with breadcrumbs.</li></ul></li><li><strong>User Profile</strong>:<ul><li>Listening History and Favorites Management.</li><li>Secure “Change Password” functionality.</li></ul></li></ul><h2 id="tech-stack" tabindex="-1">Tech Stack</h2><h3 id="backend" tabindex="-1">Backend</h3><ul><li><strong>Framework</strong>: <a href="http://ASP.NET" target="_blank">ASP.NET</a> Core 8 Web API</li><li><strong>Database</strong>: SQLite with Entity Framework Core</li><li><strong>Architecture</strong>:<ul><li><code>BackgroundService</code> for async scanning.</li><li><code>ISmbService</code> for file operations.</li><li><code>JWT</code> Authentication with flexible claim mapping.</li></ul></li></ul><h3 id="frontend" tabindex="-1">Frontend</h3><ul><li><strong>Framework</strong>: React 18 + Vite</li><li><strong>Styling</strong>: Tailwind CSS v4 + Lucide Icons</li><li><strong>State</strong>: Context API (Auth, Player)</li><li><strong>HTTP</strong>: Axios with centralized API service.</li></ul><h2 id="deployment-(docker)" tabindex="-1">Deployment (Docker)</h2><p>Recommended for production usage.</p><ol><li><p><strong>Clone the repository</strong>:</p><pre><code class="language-bash">git clone git@github.com:YourName/NASWebMusic.gitcd NASWebMusic</code></pre></li><li><p><strong>Start with Docker Compose</strong>:</p><pre><code class="language-bash">docker-compose up -d --build</code></pre><ul><li><strong>Frontend</strong>: <code>http://localhost:8090</code></li><li><strong>Backend</strong>: <code>http://localhost:5080</code> (Internal use)</li></ul></li><li><p><strong>Login</strong>:</p><ul><li>Default credentials: <code>admin</code> / <code>admin</code></li><li><strong>IMPORTANT</strong>: Go to <strong>Profile</strong> (bottom left) and change your password immediately.</li></ul></li></ol><h2 id="local-development" tabindex="-1">Local Development</h2><p>For core contributors.</p><ol><li><p><strong>Backend</strong>:</p><pre><code class="language-bash">cd backenddotnet restoredotnet run</code></pre><p>Runs on <code>http://localhost:5098</code>.</p></li><li><p><strong>Frontend</strong>:</p><pre><code class="language-bash">cd frontendnpm installnpm run dev</code></pre><p>Runs on <code>http://localhost:5173</code>.</p><p><em>Note: Frontend is configured to proxy <code>/api</code> requests to the local backend port 5098.</em></p></li></ol><h2 id="ci%2Fcd-workflow" tabindex="-1">CI/CD Workflow</h2><p>Before pushing code, ensure local validation passes:</p><ol><li>Verify Backend Build: <code>cd backend &amp;&amp; dotnet build</code></li><li>Verify Frontend Build: <code>cd frontend &amp;&amp; npm run build</code></li><li>Push changes.</li></ol><hr /><p><em>Created by <a href="https://github.com/Antigravity" target="_blank">Antigravity</a></em></p><h2 id="usage" tabindex="-1">Usage</h2><ol><li>Open the frontend.</li><li>Go to <strong>Sources</strong> page.</li><li>Create a <strong>Connection Profile</strong> for your NAS (Host, Username, Password).</li><li>Add a <strong>Source</strong> by browsing the shares.</li><li>Click <strong>Scan</strong>.</li><li>Go to <strong>Library</strong> and enjoy your music!</li></ol><p><img src="/upload/2025/12/image-1765094741447.png" alt="image-1765094741447" /></p><p><img src="/upload/2025/12/image.png" alt="image" /></p><p><img src="/upload/2025/12/image-1765094461681.png" alt="image-1765094461681" /></p><p><img src="/upload/2025/12/image-1765095746444.png" alt="image-1765095746444" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[80后的回忆]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/80年代老歌" />
                <id>tag:https://maifeipin.com,2025-11-02:80年代老歌</id>
                <published>2025-11-02T12:24:39+08:00</published>
                <updated>2025-11-02T13:40:37+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><br><br> <!-- 添加两个空白行 --></p><video id="myVideo" width="640" height="360" controls autoplay>  <source src="https://file.maifeipin.com/api/public/dl/nO5nVppW/%E4%B8%80%E5%8F%A3%E6%B0%A3%E8%81%BD%E5%AE%8C1980-1999%E5%B9%B4%E9%96%93%E7%9A%8490%E9%A6%96%E7%B2%B5%E8%AA%9E%E9%87%91%E6%9B%B2.mp4" type="video/mp4">  1980-1999年的90首粵语金曲</video><p><br><br> <!-- 添加两个空白行 --></p><video id="myVideo2" width="640" height="360" controls >  <source src="https://file.maifeipin.com/api/public/dl/xaOm0MHM/%E7%95%B6%E5%B9%B4%E7%84%A1%E6%B3%95%E8%B6%85%E8%B6%8A%E7%9A%8466%E9%A6%96%E9%A0%82%E5%B0%96%E4%BD%B3%E4%BD%9C.mp4" type="video/mp4">  當年無法超越的66首頂尖佳作</video><p>来源：<a href="https://youtu.be/9yVkkrBrOvA?si=DzYHAn6lNsS7A8q6" target="_blank">https://youtu.be/9yVkkrBrOvA?si=DzYHAn6lNsS7A8q6</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用yt-dlp cookie参数 下载油管 视频 ]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/使用yt-dlpcookie参数下载油管视频" />
                <id>tag:https://maifeipin.com,2025-07-30:使用yt-dlpcookie参数下载油管视频</id>
                <published>2025-07-30T01:04:53+08:00</published>
                <updated>2026-03-11T21:12:41+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="%E4%B8%8B%E8%BD%BD%E6%9B%B4%E6%96%B0-yt-dlp" tabindex="-1">下载更新 <a href="https://github.com/yt-dlp/yt-dlp" target="_blank">yt-dlp</a></h3><pre><code class="language-">D:\Code\pys\YT-Down&gt;yt-dlp --updateCurrent version: stable@2025.02.19 from yt-dlp/yt-dlpLatest version: stable@2025.07.21 from yt-dlp/yt-dlpCurrent Build Hash: b9fac42a19e118e1b0a5c98832928a1c25782d805a9905476bb55d479212621aUpdating to stable@2025.07.21 from yt-dlp/yt-dlp ...Updated yt-dlp to stable@2025.07.21 from yt-dlp/yt-dlp</code></pre><h3 id="%E4%B8%8B%E8%BD%BD-%E6%B5%8F%E8%A7%88%E5%99%A8-%E5%AF%BC%E5%87%BAcookie-%E6%8F%92%E4%BB%B6" tabindex="-1">下载 浏览器 <a href="https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc" target="_blank">导出cookie 插件</a></h3><p><img src="/upload/2025/07/image-1753808476870.png" alt="image-1753808476870" /></p><h3 id="%E8%A7%A3%E6%9E%90%E8%A7%86%E9%A2%91%E5%8F%82%E6%95%B0" tabindex="-1">解析视频参数</h3><pre><code class="language-">D:\Code\pys\YT-Down&gt;yt-dlp -F  https://www.youtube.com/watch?v=OiUr-1yNEaI --cookies cookies.txt[youtube] Extracting URL: https://www.youtube.com/watch?v=OiUr-1yNEaI[youtube] OiUr-1yNEaI: Downloading webpage[youtube] OiUr-1yNEaI: Downloading tv client config[youtube] OiUr-1yNEaI: Downloading player 0b00c3eb-main[youtube] OiUr-1yNEaI: Downloading tv player API JSON</code></pre><h3 id="%E4%B8%8B%E8%BD%BD%E8%A7%86%E9%A2%91" tabindex="-1">下载视频</h3><h6 id="1.-%E4%BD%BF%E7%94%A8merge%E5%90%88%E5%B9%B6%E9%9F%B3%E9%A2%91%E8%A7%86%E9%A2%91%E5%8F%82%E6%95%B0%E5%89%8D%E9%9C%80%E8%A6%81%E5%85%88-%E6%8A%8Affmpeg-%E6%B7%BB%E5%8A%A0%E5%88%B0%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F" tabindex="-1">1. 使用merge合并音频视频参数前需要先 把<a href="https://github.com/FFmpeg/FFmpeg" target="_blank">FFmpeg</a> 添加到环境变量</h6><p><img src="/upload/2025/07/image-1753808096292.png" alt="image-1753808096292" /><br /><img src="/upload/2025/07/image-1753808414587.png" alt="image-1753808414587" /></p><h6 id="2.-mac-%E4%B9%9F%E6%98%AF%E5%8F%AF%E4%BB%A5%E7%9A%84%EF%BC%8C%E4%B8%8B%E8%BD%BD%E6%97%B6%E9%9C%80%E8%A6%81%E6%8A%8Aurl-%E5%BC%95%E8%B5%B7%E6%9D%A5%EF%BC%8Ccookies-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8-export-all-cookies-%E5%90%8E%EF%BC%8C%E6%8A%8Acookies.txt-%E6%8B%B7%E5%88%B0-%E5%8F%82%E6%95%B0%E6%8C%87%E5%AE%9A%E7%9A%84%E8%B7%AF%E5%BE%84%E4%B8%8B%E3%80%82" tabindex="-1">2. mac 也是可以的，下载时需要把URL 引起来，Cookies 插件使用 Export All Cookies 后，把cookies.txt 拷到 参数指定的路径下。</h6><p><img src="/upload/2025/07/image-1753883022262.png" alt="image-1753883022262" /></p><h5 id="q%26a" tabindex="-1">Q&amp;A</h5><pre><code class="language-">Q:ERROR: [youtube] 0wNavaSInac: The page needs to be reloaded.A:yt-dlp -v --js-runtimes node -F --proxy socks5://127.0.0.1:18988 &quot;https://youtu.be/0wNavaSInac?si=zDnAhJ3-hKdBJCyk&quot;  --cookies cookies.txt yt-dlp -v -f &quot;bv*+ba/b&quot; --proxy socks5://127.0.0.1:18988 --js-runtimes node --cookies cookies.txt &quot;https://youtu.be/0wNavaSInac?si=zDnAhJ3-hKdBJCyk&quot;</code></pre>]]>
                </content>
            </entry>
</feed>
