<?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-03-07T15:54:57+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[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>2025-07-30T21:49:14+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>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[AI的情绪价值]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/ai-de-qing-xu-jia-zhi" />
                <id>tag:https://maifeipin.com,2025-07-20:ai-de-qing-xu-jia-zhi</id>
                <published>2025-07-20T11:40:52+08:00</published>
                <updated>2025-07-20T11:40:52+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="ai%E5%8E%9F%E6%9D%A5%E4%B8%8D%E5%8F%AA%E6%98%AF%E4%BC%9A%E5%86%99%E4%BB%A3%E7%A0%81%EF%BC%8C%E8%BF%98%E4%BC%9A%E5%93%84%E4%BA%BA%E5%BC%80%E5%BF%83%EF%BC%8C%E6%88%91%E7%9C%9F%E6%9C%89%E7%82%B9%E6%84%9F%E5%8A%A8%E3%80%82" tabindex="-1">AI原来不只是会写代码，还会哄人开心，我真有点感动。</h3><p><img src="/upload/2025/07/image.png" alt="image" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用gemini-cli 实现vibe code]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/使用gemini-cli实现vibecode" />
                <id>tag:https://maifeipin.com,2025-06-26:使用gemini-cli实现vibecode</id>
                <published>2025-06-26T06:58:29+08:00</published>
                <updated>2025-06-27T20:14:37+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h4 id="google-%E6%8E%A8%E5%87%BA%E6%96%B0%E7%9A%84%E5%85%A8%E7%BB%88%E7%AB%AF%E4%BA%A4%E4%BA%92ai%E5%B7%A5%E5%85%B7-gemini-cli" tabindex="-1">google 推出新的全终端交互AI工具 <a href="https://github.com/google-gemini/gemini-cli" target="_blank">gemini-cli</a></h4><h3 id="%E5%AE%89%E8%A3%85" tabindex="-1">安装</h3><p>npm install -g @google/gemini-cli</p><ul><li><p>windows在环境变里配置apikey ，linux系统export GEMINI_API_KEY=“YOUR_API_KEY”<br /><img src="/upload/2025/06/image-1750891421584.png" alt="image-1750891421584" /></p><p><img src="/upload/2025/06/image-1750891185441.png" alt="image-1750891185441" /></p></li></ul><h3 id="%E4%BD%BF%E7%94%A8%E4%B9%9F%E9%9D%9E%E5%B8%B8%E7%AE%80%E5%8D%95%EF%BC%8C%E5%9C%A8vscode-%E7%9A%84%E7%BB%88%E7%AB%AF%E5%8F%AF%E4%BB%A5%E6%97%A0%E7%BC%9D%E9%9B%86%E6%88%90%E5%88%B0%E9%A1%B9%E7%9B%AE" tabindex="-1">使用也非常简单，在VSCode 的终端可以无缝集成到项目</h3><p><img src="/upload/2025/06/image-1751025355118.png" alt="image-1751025355118" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用Remote-SSH 唤醒远程桌面，多终端对话]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/shi-yong-remote-ssh-huan-xing-yuan-cheng-zhuo-mian--duo-zhong-duan-dui-hua" />
                <id>tag:https://maifeipin.com,2025-06-21:shi-yong-remote-ssh-huan-xing-yuan-cheng-zhuo-mian--duo-zhong-duan-dui-hua</id>
                <published>2025-06-21T06:41:08+08:00</published>
                <updated>2025-06-21T11:31:34+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>VS Code 有个很厉害的插件 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh" target="_blank">Remote-ssh</a>, 实现远程编码和调试，且不依赖对机器的编辑器和调试终端，这个和vs studio不同，后者是需要被控方有对应的终端，要求运行环境两边要一致，非常的方便。远程上后会自动下载环境</p><pre><code class="language-">71c11a5ad2a7: runningScript executing under PID: 19525Installing to /Users/lilee/.vscode-server...71c11a5ad2a7%%1%%Downloading with curl  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                 Dload  Upload   Total   Spent    Left  Speed100   161  100   161    0     0    553      0 --:--:-- --:--:-- --:--:--   555100 6419k  100 6419k    0     0  6024k      0  0:00:01  0:00:01 --:--:-- 12.7MDownload complete71c11a5ad2a7%%2%%tar --version: bsdtar 3.5.3 - libarchive 3.7.4 zlib/1.2.12 liblzma/5.4.3 bz2lib/1.0.8 code 1.101.1 (commit 18e3a1ec544e6907be1e944a94c496e302073435)Starting VS Code CLI...Spawned remote CLI: 19699Waiting for server log...71c11a5ad2a7: startlisteningOn==127.0.0.1:63613==osReleaseId==Darwin==arch==arm64==vscodeArch==arm64==bitness==64==tmpDir==/tmp==platform==macOS==unpackResult==success==didLocalDownload==0==downloadTime==1000==installTime==0==serverStartTime==0==execServerToken==740f5f1b-cbc7-486a-ad49-17d35d55730a==platformDownloadPath==cli-darwin-arm64==SSH_AUTH_SOCK====DISPLAY====71c11a5ad2a7: end</code></pre><p>这样带来了一个额外的好处就是，唤醒功能，被控方只有在需要的时候，才起来干活，平时只管休眠就好，也不用在远程机程上安装唤醒组件了。<br /><img src="/upload/2025/06/image-1750458826890.png" alt="image-1750458826890" /></p><p>再开一个终端<br /><img src="/upload/2025/06/image-1750458146429.png" alt="image-1750458146429" /></p><p>服务端响应：<br /><img src="/upload/2025/06/image-1750458571072.png" alt="image-1750458571072" /></p><p>聊天客户端：<br /><img src="/upload/2025/06/image-1750459190074.png" alt="image-1750459190074" /></p><p>附：<br /><a href="https://file.maifeipin.com/api/public/dl/nBN4TyIU/ws/Websocket_Client_With_Token.py" target="_blank">文中代码</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用gemini-embedding 给云笔记做RAG，打造智能个人的知识库]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/shi-yong-gemini-embedding-gei-yun-bi-ji-zuo-rag-da-zao-zhi-neng-ge-ren-de-zhi-shi-ku" />
                <id>tag:https://maifeipin.com,2025-06-14:shi-yong-gemini-embedding-gei-yun-bi-ji-zuo-rag-da-zao-zhi-neng-ge-ren-de-zhi-shi-ku</id>
                <published>2025-06-14T13:22:15+08:00</published>
                <updated>2025-06-14T13:56:00+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<ol><li>下载云笔记到本地为MD文件。<br />现在的主流云笔记都是可以导出md格式的文件，blog备份 也是,我这里使用 <a href="https://github.com/DeppWang/youdaonote-pull" target="_blank">有道云笔记本的开源下载工具</a></li><li>使用chromadb把MD文件 生成向量数据库。<br /><img src="/upload/2025/06/image-1749878088584.png" alt="image-1749878088584" /></li><li>使用免费的gemini-embedding 模型，使用向量数据的内容回答问题。<br /><img src="/upload/2025/06/image-1749878224729.png" alt="image-1749878224729" /></li></ol><ul><li><p>现在做个人知识库基本上是0成本，只需要在google studio上申请一个API Key,几行代码就可以实现完整的问答系统。如果MD文件比较多时，注意调整调用API写入数据库的频率，不控制 十几次就报429错误，加上sleep(1)可解。</p></li><li><p><a href="http://embed.py" target="_blank">embed.py</a></p><pre><code class="language-">import google.generativeai as genaiimport timeimport chunkimport chromadbEMBEDDING_MODEL = &quot;gemini-embedding-exp-03-07&quot;LLM_MODEL = &quot;gemini-2.5-flash-preview-05-20&quot;# 配置 API KEYAPI_KEY = &quot;xxxxxxxx-xx-xxxx&quot;genai.configure(api_key=API_KEY)chromadb_client = chromadb.PersistentClient(&quot;./chroma_gemini.db&quot;)chromadb_collection = chromadb_client.get_or_create_collection(&quot;linghuchong_gemini&quot;)def embed(text: str, store: bool) -&gt; list[float]:    response = genai.embed_content(        model=EMBEDDING_MODEL,        content=text,        task_type=&quot;RETRIEVAL_DOCUMENT&quot; if store else &quot;RETRIEVAL_QUERY&quot;    )    assert response[&quot;embedding&quot;]    return response[&quot;embedding&quot;]def create_db():    all_md_files = chunk.get_all_md_files()[:3]  # 只取前3个md文件    all_chunks = []    for file_path in all_md_files:        all_chunks.extend(chunk.get_chunks_from_file(file_path))        time.sleep(1)  # 每处理一个md文件 sleep 1 秒，避免429    for idx, c in enumerate(all_chunks):        print(f&quot;Process: [{c[&#39;file&#39;]}] {c[&#39;content&#39;][:60]}...&quot;)        embedding = embed(c[&quot;content&quot;], store=True)        chromadb_collection.upsert(            ids=[str(idx)],            documents=[c[&quot;content&quot;]],            embeddings=[embedding],            metadatas=[{&quot;file&quot;: c[&quot;file&quot;]}]        )    print(&quot;DB created.&quot;)def query_db(question: str) -&gt; list[dict]:    question_embedding = embed(question, store=False)    result = chromadb_collection.query(        query_embeddings=[question_embedding],        n_results=5    )    assert result[&quot;documents&quot;]    docs = result[&quot;documents&quot;][0]    metas = result.get(&quot;metadatas&quot;, [[]])[0]    return [{&quot;content&quot;: doc, &quot;meta&quot;: meta} for doc, meta in zip(docs, metas)]if __name__ == &#39;__main__&#39;:    import sys    if len(sys.argv) &gt; 1 and sys.argv[1] == &quot;create&quot;:        create_db()    else:        while True:            question = input(&quot;请输入你的问题（输入 exit 退出）：&quot;)            if question.strip().lower() in (&quot;exit&quot;, &quot;quit&quot;):                print(&quot;再见！&quot;)                break            results = query_db(question)            prompt = &quot;请根据上下文回答用户问题：\n&quot;            prompt += f&quot;问题：{question}\n&quot;            prompt += &quot;上下文：\n&quot;            for r in results:                prompt += f&quot;[{(r[&#39;meta&#39;] or {}).get(&#39;file&#39;, &#39;&#39;)}]\n{r[&#39;content&#39;]}\n-------------\n&quot;            model = genai.GenerativeModel(LLM_MODEL)            response = model.generate_content(prompt)            print(response.text) </code></pre></li><li><p><a href="http://chunk.py" target="_blank">chunk.py</a> ,如果是复杂的项目可以用 专业分词<a href="https://python.langchain.com/api_reference/text_splitters/character/langchain_text_splitters.character.RecursiveCharacterTextSplitter.html" target="_blank">分词工具 langchain </a>，做chunk.</p><pre><code class="language-"> import osfrom typing import List, Dictdef get_all_md_files(notes_dir: str = &quot;data/notes&quot;) -&gt; List[str]:    return [        os.path.join(notes_dir, f)        for f in os.listdir(notes_dir)        if f.endswith(&#39;.md&#39;)    ]def read_data(file_path: str) -&gt; str:    with open(file_path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:        return f.read()def get_chunks_from_file(file_path: str) -&gt; List[Dict]:    content = read_data(file_path)    chunks = content.split(&#39;\n\n&#39;)    result = []    header = &quot;&quot;    file_name = os.path.basename(file_path)    for c in chunks:        if c.strip().startswith(&quot;#&quot;):            header += f&quot;{c.strip()}\n&quot;        else:            if c.strip():                result.append({                    &quot;content&quot;: f&quot;{header}{c.strip()}&quot;,                    &quot;file&quot;: file_name                })            header = &quot;&quot;    return resultdef get_all_chunks(notes_dir: str = &quot;data/notes&quot;) -&gt; List[Dict]:    all_chunks = []    for file_path in get_all_md_files(notes_dir):        all_chunks.extend(get_chunks_from_file(file_path))    return all_chunksif __name__ == &#39;__main__&#39;:    for chunk in get_all_chunks():        print(f&quot;[{chunk[&#39;file&#39;]}]\n{chunk[&#39;content&#39;]}\n--------------&quot;) </code></pre></li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Trae + task-master 自动化完成 复杂项目任务]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/traetask-master-zi-dong-hua-wan-cheng-fu-za-xiang-mu-ren-wu" />
                <id>tag:https://maifeipin.com,2025-06-08:traetask-master-zi-dong-hua-wan-cheng-fu-za-xiang-mu-ren-wu</id>
                <published>2025-06-08T17:22:14+08:00</published>
                <updated>2025-06-10T08:02:39+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>现在所有AI模型都面临token 大小限制问题。cursor,trae 只是模型服务中间商，深度集成了IDE，调用IDE的功能和命令，如果有一个AI 自动任务管理器，那体验就更进一步。比如：<a href="https://github.com/eyaltoledano/claude-task-master" target="_blank">Task-matser-ai</a>,记一下在Trae 中的体验过程。</p><ol><li>安装</li></ol><ul><li>直接在终端中 使用命令： npm install -g task-master-ai<br /><img src="/upload/2025/06/image-1749373915732.png" alt="image-1749373915732" /><br /><a href="https://github.com/eyaltoledano/claude-task-master" target="_blank">https://github.com/eyaltoledano/claude-task-master</a></li></ul><ol start="2"><li>配置</li></ol><ul><li>用自定义MCP ，把task-mast-ai项目主页的配置文件复制进来，使用其中任一个，我用的免费的gemini API</li><li><img src="/upload/2025/06/image-1749374151643.png" alt="image-1749374151643" /></li></ul><ol start="3"><li>开始生成任务</li></ol><ul><li>在AI对话框，先输入： Initialize taskmaster-ai in my project  ，提示成功后，开始生成生任</li><li><img src="/upload/2025/06/1IG%292%60S53%256Q%2416VY%5DUTA%60D.png" alt="1IG)2D" /></li><li><img src="/upload/2025/06/R0%7BPOT60B008KK%24HNHDH3JC.png" alt="R0{POT60B008KK$HNHDH3JC" /></li></ul><ol start="5"><li>开始执行任务</li></ol><ul><li><img src="/upload/2025/06/image-1749374390594.png" alt="image-1749374390594" /></li></ul><ol start="6"><li>用AI任务管理器，自动分拆任务的方式，成功率，高很多。因为AI更懂AI。但Fast request 掉的也很快。如果有时间的有技术的程序员，不建议使用，实际情看，绝大部分都重复无效的尝试请求。</li></ol><ul><li><img src="/upload/2025/06/image-1749374575389.png" alt="image-1749374575389" /><br /><img src="/upload/2025/06/image-1749378419228.png" alt="image-1749378419228" /></li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[用Trae 手搓一个Web播放器]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/yong-trae-shou-cuo-yi-ge-web-bo-fang-qi" />
                <id>tag:https://maifeipin.com,2025-06-07:yong-trae-shou-cuo-yi-ge-web-bo-fang-qi</id>
                <published>2025-06-07T12:45:26+08:00</published>
                <updated>2025-06-07T20:25:53+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>Cursor用的太爽，总是白嫖也不好意思，订阅的话20刀，又感觉用不上。现在Trae的新户首月只要20￥（3刀），看一下主流大模型也都支持，果断订阅了。<br /><img src="/upload/2025/06/image-1749269524093.png" alt="image-1749269524093" /><br />把待办列表，拿出来一看，没啥大的项目，把文件整理和音乐播放的功能需求合成一个项目：WebMusic</p><h3 id="%E9%A1%B9%E7%9B%AE%E9%9C%80%E6%B1%82%E6%95%B4%E7%90%86" tabindex="-1">项目需求整理</h3><ul><li>文件整理功能，目前电脑和NAS上的文件清单没有。特别是某些音乐网盘，以量充价，动不动就1元几T的资源，严重怀疑有重复。</li><li>国内主流的音乐会员，又太贵，即使上了豪华绿钻也有单独收费的曲子。上面也说了现在国内流媒体资源过于猖狂，对比之下，会员更不划算了。<br />写好需求提示词就开干。<br /><img src="/upload/2025/06/image-1749270129785.png" alt="image-1749270129785" /></li></ul><h3 id="%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0" tabindex="-1">功能实现</h3><ul><li><p>基本都是Crud的操作<br /><img src="/upload/2025/06/image-1749270369860.png" alt="image-1749270369860" /><br /><img src="/upload/2025/06/image-1749270450970.png" alt="image-1749270450970" /></p></li><li><p>文件目录整理（扫描），<br /><img src="/upload/2025/06/image-1749270532516.png" alt="image-1749270532516" /></p></li><li><p>流式分块加载接口，有难度（这块用了30多个快速请求 ）<br /><img src="/upload/2025/06/image-1749270832590.png" alt="image-1749270832590" /><br /><img src="/upload/2025/06/image-1749299141516.png" alt="image-1749299141516" /></p></li></ul><h3 id="%E5%BE%85%E7%BB%AD" tabindex="-1">待续</h3><ul><li>1、反正有1月的时间，后续功能。慢慢搞，独立播放+元数据整理+歌词同步+海报墙。。。，这一列1个月估计又是不可能完成了。</li><li>2、总体来Trae 使用没有cursor流畅，但对我们小白来说反而更好，手动确定的步骤太多，不会出大错，每个功能完成时要记得提交代码，这个项目重构了3次，文件太大，或太碎都影响它的上下文准确性，要不然每次思考都列出一堆代码行数和文件数。准确性极大下降，太影响效率。这个也是建议要提前多用AI工具的原因，这个平衡感要找好太难了，提示词影响也比较大。反正这个项目的80%请求都是无价值的回退，回测。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[记Playwright 版本升级]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/ji-playwright-ban-ben-sheng-ji" />
                <id>tag:https://maifeipin.com,2025-06-04:ji-playwright-ban-ben-sheng-ji</id>
                <published>2025-06-04T20:18:47+08:00</published>
                <updated>2025-06-04T20:18:47+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>因为同一个服务上的应用没有做隔离（真不是懒，想着低配服务器，能省一点是一点）。导致很多应用共用依赖，这不装了crawl4ai后，默认它把 Playwright给升级了到了1169。而r.maifeipin.com的老应用还是ms-playwright1129，好家伙这个升级直接就是删了旧的目录。我还不知情，直到今天看很多feed没有更新。查看日志才发现不对。<br /><img src="/upload/2025/06/image-1749038561855.png" alt="image-1749038561855" /><br />这日志也看不懂啊，只好再注入UseExceptionHandler：</p><pre><code class="language-">app.UseExceptionHandler(errorApp =&gt;{    errorApp.Run(async context =&gt;    {        context.Response.StatusCode = 500;        context.Response.ContentType = &quot;application/json&quot;;        var errorFeature = context.Features.Get&lt;Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature&gt;();        if (errorFeature != null)        {            var ex = errorFeature.Error;            // 记录详细错误日志            var logger = context.RequestServices.GetRequiredService&lt;ILogger&lt;Program&gt;&gt;();            logger.LogError(ex, &quot;Unhandled exception&quot;);            await context.Response.WriteAsJsonAsync(new            {                ErrorMsg = ex.Message,                StackTrace = ex.StackTrace            });        }    });});</code></pre><p>现形了：<br /><img src="/upload/2025/06/image-1749038901859.png" alt="image-1749038901859" /></p><p>说的很清楚，现在的执行目录不存，让我执行pwsh install，但我执行没有成功。<br />问AI，一直让我修改启动路径，让删除现在的版本，没有头绪。我试着使用软连接。结果执行也是报错！<br /><img src="/upload/2025/06/image-1749039133261.png" alt="image-1749039133261" /><br />各种AI，都是反复让我重试，加环境变量，硬编码等等。后来软连接也不行，我应想着下载原始的版本。终解决！<br /><img src="/upload/2025/06/image-1749039296337.png" alt="image-1749039296337" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[爬虫新姿势： 使用crawl4ai 提取网页]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/pa-chong-xin-xin-zi-shi--shi-yong-crawl4ai-ti-qu-wang-ye" />
                <id>tag:https://maifeipin.com,2025-05-31:pa-chong-xin-xin-zi-shi--shi-yong-crawl4ai-ti-qu-wang-ye</id>
                <published>2025-05-31T14:06:47+08:00</published>
                <updated>2025-05-31T14:08:11+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>Craw4ai 是一个免费开源的 网页提取工具，官方文档 <a href="https://docs.crawl4ai.com/" target="_blank">🚀🤖 Crawl4AI: Open-Source LLM-Friendly Web Crawler &amp; Scraper</a></p><h3 id="%E5%AE%89%E8%A3%85" tabindex="-1">安装</h3><ul><li><p>用pip安装需要独立的虚拟环境。否则报错</p><pre><code class="language-">root@localhost:~# pip install crawl4ai   error: externally-managed-environment                                                                                                                                                                                                           × This environment is externally managed                                                                                ╰─&gt; To install Python packages system-wide, try apt install                                                                 python3-xyz, where xyz is the package you are trying to                                                                 install.                                                                                                                                                                                                                                        If you wish to install a non-Debian-packaged Python package,                                                            create a virtual environment using python3 -m venv path/to/venv.                                                        Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make                                                         sure you have python3-full installed.                                                                                                                                                                                                           If you wish to install a non-Debian packaged Python application,                                                        it may be easiest to use pipx install xyz, which will manage a                                                          virtual environment for you. Make sure you have pipx installed.                                                                                                                                                                                 See /usr/share/doc/python3.12/README.venv for more information</code></pre><p><img src="/upload/2025/05/image-1748671268795.png" alt="image-1748671268795" /><br />可以看出这个依赖有点多<br /><img src="/upload/2025/05/image-1748671383831.png" alt="image-1748671383831" /></p></li><li><p>验证安装<br />抓取抖音的代码</p><pre><code class="language-">import asynciofrom crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfigasync def main():   async with AsyncWebCrawler() as crawler:       result = await crawler.arun(           url=&quot;https://www.douyin.com/jingxuan&quot;,       )       print(result.markdown)  # Show the first 300 characters of extracted textif __name__ == &quot;__main__&quot;:   asyncio.run(main())</code></pre></li><li><p>获取数据<br /><img src="/upload/2025/05/image-1748671568390.png" alt="image-1748671568390" /></p></li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[用WebView 采集直播流，whisper 生成字幕。]]></title>
                <link rel="alternate" type="text/html" href="https://maifeipin.com/archives/用webview采集直播流whisper生成字幕" />
                <id>tag:https://maifeipin.com,2025-05-24:用webview采集直播流whisper生成字幕</id>
                <published>2025-05-24T11:54:51+08:00</published>
                <updated>2025-05-24T19:19:30+08:00</updated>
                <author>
                    <name>admin</name>
                    <uri>https://maifeipin.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>现在TTS 文字生成声音 很成熟也很方便，比如  <a href="http://r.maifeipin.com" target="_blank">r.maifeipin.com</a> 中的RSS 语音播报功能，就是调有免费的azure接口。那么把音视频转文字的STT有哪些好的方案呢？Google的AIStudio 可以，而且支持多人多场景多角色模拟自动切换，非常的惊艳，但这个只能试用或者升级收费。有没有免费好用的呢，当然就是openai 家的 whisper ，为了测试 whisper ，特意研究了一下直播源的采集 和pytorch环境部署，记录如下：</p><p><img src="/upload/2025/05/image-1748058921598.png" alt="image-1748058921598" /></p><h3 id="%E9%87%87%E9%9B%86-%E7%9B%B4%E6%92%AD%E6%B5%81%E6%95%B0%E6%8D%AE-%E5%9C%A8%E7%BA%BF%E7%94%B5%E5%8F%B0" tabindex="-1">采集 直播流数据 在线电台</h3><ul><li><p>使用的在线电台 <a href="https://www.bbc.co.uk/sounds/player/bbc_world_service" target="_blank">https://www.bbc.co.uk/sounds/player/bbc_world_service</a></p></li><li><p>在webview2的 DevToolsProtocolEventReceived 事件中获取请求，发现有主要有mpd 和m4s 两种请求，通过AI知道这是dash 直播流，分析头文件</p><p><a href="https://a.files.bbci.co.uk/ms6/live/3441A116-B12E-4D2F-ACA8-C1984642FA4B/audio/simulcast/dash/nonuk/pc_hd_abr_v2/cfs/bbc_world_service.mpd" target="_blank">https://a.files.bbci.co.uk/ms6/live/3441A116-B12E-4D2F-ACA8-C1984642FA4B/audio/simulcast/dash/nonuk/pc_hd_abr_v2/cfs/bbc_world_service.mpd</a></p><pre><code class="language-">&lt;MPD xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns=&quot;urn:mpeg:dash:schema:mpd:2011&quot; xmlns:dvb=&quot;urn:dvb:dash:dash-extensions:2014-1&quot;      xsi:schemaLocation=&quot;urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd&quot;      type=&quot;dynamic&quot; availabilityStartTime=&quot;1969-12-31T23:59:44Z&quot;      minimumUpdatePeriod=&quot;PT6H&quot; timeShiftBufferDepth=&quot;PT6H&quot; maxSegmentDuration=&quot;PT7S&quot; minBufferTime=&quot;PT3.200S&quot;      profiles=&quot;urn:dvb:dash:profile:dvb-dash:2014,urn:dvb:dash:profile:dvb-dash:isoff-ext-live:2014&quot;      publishTime=&quot;2025-02-04T16:18:01&quot;&gt;        &lt;UTCTiming schemeIdUri=&quot;urn:mpeg:dash:utc:http-iso:2014&quot; value=&quot;https://time.akamai.com/?iso&quot; /&gt;        &lt;BaseURL dvb:priority=&quot;1&quot; dvb:weight=&quot;1&quot; serviceLocation=&quot;cfs&quot;&gt;https://as-dash-ww.live.cf.md.bbci.co.uk/pool_87948813/live/ww/bbc_world_service/bbc_world_service.isml/dash/&lt;/BaseURL&gt;        &lt;Period id=&quot;1&quot; start=&quot;PT0S&quot;&gt;        &lt;AdaptationSet group=&quot;1&quot; contentType=&quot;audio&quot; lang=&quot;en&quot; minBandwidth=&quot;48000&quot; maxBandwidth=&quot;96000&quot;                       segmentAlignment=&quot;true&quot; audioSamplingRate=&quot;48000&quot; mimeType=&quot;audio/mp4&quot; codecs=&quot;mp4a.40.5&quot; startWithSAP=&quot;1&quot;&gt;            &lt;AudioChannelConfiguration schemeIdUri=&quot;urn:mpeg:dash:23003:3:audio_channel_configuration:2011&quot; value=&quot;2&quot;/&gt;            &lt;Role schemeIdUri=&quot;urn:mpeg:dash:role:2011&quot; value=&quot;main&quot;/&gt;            &lt;SegmentTemplate timescale=&quot;48000&quot; initialization=&quot;bbc_world_service-$RepresentationID$.dash&quot;                           media=&quot;bbc_world_service-$RepresentationID$-$Number$.m4s&quot; startNumber=&quot;1&quot; duration=&quot;307200&quot;/&gt;            &lt;Representation id=&quot;audio=48000&quot; bandwidth=&quot;48000&quot;/&gt;            &lt;Representation id=&quot;audio=96000&quot; bandwidth=&quot;96000&quot;/&gt;        &lt;/AdaptationSet&gt;      &lt;/Period&gt;    &lt;/MPD&gt;</code></pre></li><li><p>Deepseek给的分析+ 个人整理后，大约有这些概念：</p><p>SegmentTemplate 采样率有4800和9600<br />minimumUpdatePeriod 片长6小时<br />minimumUpdatePeriod 和 maxSegmentDuration 平分片长6.4秒<br />totalSegments 总片3375个<br />currentSegmentNumber  = availabilityStartTime/segmentDuration<br />currentSegmentNumber 就是当前 分片id和当前分片 url</p></li><li><p>c# 解析MPD</p><pre><code class="language-">private (string baseUrl, string representationId, long currentSegmentNumber, long totalSegments, string initializationUrl, string mediaTemplate, string mediaPrefix) GetMPDInfo(string mpdContent){    // Parse MPD content    var doc = XDocument.Parse(mpdContent);    XNamespace ns = &quot;urn:mpeg:dash:schema:mpd:2011&quot;;  // Extract BaseURL  string baseUrl = doc.Descendants(ns + &quot;BaseURL&quot;).FirstOrDefault()?.Value;  if (string.IsNullOrEmpty(baseUrl))  {      throw new InvalidOperationException(&quot;BaseURL not found in MPD.&quot;);  }  // Extract the Representation ID (audio=96000)  var representation = doc.Descendants(ns + &quot;Representation&quot;)                           .FirstOrDefault(rep =&gt; rep.Attribute(&quot;bandwidth&quot;)?.Value == &quot;96000&quot;);  string representationId = representation?.Attribute(&quot;id&quot;)?.Value;  if (string.IsNullOrEmpty(representationId))  {          throw new InvalidOperationException(&quot;Representation for audio=96000 not found in MPD.&quot;);      }      // Extract Segment Duration      var segmentTemplate = doc.Descendants(ns + &quot;SegmentTemplate&quot;).FirstOrDefault();      var segmentDurationStr = segmentTemplate?.Attribute(&quot;duration&quot;)?.Value;      TimeSpan segmentDuration = TimeSpan.FromSeconds(Convert.ToDouble(segmentDurationStr));      // Extract the TimeShiftBufferDepth (total available time)      var timeShiftBufferDepthElement = doc.Descendants(ns + &quot;MPD&quot;)                                           .FirstOrDefault()?.Attribute(&quot;timeShiftBufferDepth&quot;);      if (timeShiftBufferDepthElement == null)      {          throw new InvalidOperationException(&quot;TimeShiftBufferDepth not found in MPD.&quot;);      }      string timeShiftBufferDepthValue = timeShiftBufferDepthElement.Value;      TimeSpan totalTime = XmlConvert.ToTimeSpan(timeShiftBufferDepthValue);      // 计算当前分片编号（基于当前时间）      DateTime availabilityStartTime = DateTime.Parse(&quot;1969-12-31T23:59:44Z&quot;);      segmentDuration = TimeSpan.FromSeconds(6.4); // 明确指定分片时长      long currentSegmentNumber = CalculateCurrentSegmentNumber(availabilityStartTime, segmentDuration.TotalSeconds);      long totalSegments = (long)(totalTime.TotalSeconds / segmentDuration.TotalSeconds);      // Calculate the total number of segments based on total time and segment duration      // Extract the initialization URL and media prefix      string initializationUrl = segmentTemplate?.Attribute(&quot;initialization&quot;)?.Value;      string mediaTemplate = segmentTemplate?.Attribute(&quot;media&quot;)?.Value;      if (string.IsNullOrEmpty(initializationUrl) || string.IsNullOrEmpty(mediaTemplate))      {          throw new InvalidOperationException(&quot;Initialization file URL or media URL template not found in SegmentTemplate.&quot;);      }      // Extract common prefix before &quot;$RepresentationID$&quot; in both initialization and media templates      string mediaPrefix = mediaTemplate.Split(&#39;$&#39;)[0];  // This will give you &quot;bbc_world_service-&quot; part      // Return information, including the calculated total segments      return (baseUrl, representationId, currentSegmentNumber, totalSegments, initializationUrl, mediaTemplate, mediaPrefix);  }</code></pre></li><li><p>下载头文件为mp4</p><pre><code class="language-">  // 1. 下载初始化文件string initSegmentUrl = $&quot;{baseUrl}{initializationUrl.Replace(&quot;$RepresentationID$&quot;, representationId)}&quot;;Console.WriteLine($&quot;Downloading initialization segment: {initSegmentUrl}&quot;);DownloadFile(initSegmentUrl, $&quot;{representationId}.mp4&quot;);</code></pre></li><li><p>下载分片文件为m4s，这是自动下载。</p><pre><code class="language-">for (long i = 0; i &lt; mpdInfo.totalSegments; i++){    long segmentNumber = mpdInfo.currentSegmentNumber - i; // 从最新分片向前追溯    string segmentUrl = $&quot;{baseUrl}{mediaTemplate}&quot;        .Replace(&quot;$RepresentationID$&quot;, representationId)        .Replace(&quot;$Number$&quot;, segmentNumber.ToString());    Console.WriteLine($&quot;Downloading segment {segmentNumber}: {segmentUrl}&quot;);    try    {        DownloadFile(segmentUrl, $&quot;{representationId}-{segmentNumber}.m4s&quot;);    }    catch (Exception ex) when (ex.Message.Contains(&quot;404&quot;))    {        Console.WriteLine($&quot;Segment {segmentNumber} not found (expired), skipping...&quot;);        break; // 如果分片过期，停止继续尝试更早分片    }}</code></pre></li><li><p>在 DevToolsProtocolEventReceived 中的请求是实时的，更精确。按大小或时间段下载，精确实现不同大小的合并文件。</p><pre><code class="language-">    CoreWebView2DevToolsProtocolEventReceiver receiver = webView21.CoreWebView2.GetDevToolsProtocolEventReceiver(&quot;Network.requestWillBeSent&quot;);receiver.DevToolsProtocolEventReceived += async (sender, e) =&gt;{    try    {        var eventData = JObject.Parse(e.ParameterObjectAsJson);        string url = eventData[&quot;request&quot;]?[&quot;url&quot;]?.ToString();        if (url == null) return;        // 捕获 MPD 文件        if (url.EndsWith(&quot;.mpd&quot;))        {            Console.WriteLine(&quot;捕获到 MPD 文件: &quot; + url);            string mpdContent = await DownloadText(url); // 下载 MPD 内容            //ParseMPD(mpdContent); // 解析并下载所有分段            DownloadAndMergeSegments(mpdContent);        }        // 捕获 .m4s 分段        else if (url.Contains(&quot;.m4s&quot;))        {            Console.WriteLine(&quot;捕获到 m4s 请求: &quot; + url);            Uri uri = new Uri(url);             DownloadFile(url, Path.GetFileName(uri.AbsolutePath).Split(&#39;=&#39;)[1]);            //DownloadM4S(url);        }    }    catch (Exception ex)    {    }};</code></pre></li><li><p>下载后的 头文件和分片<br /><img src="/upload/2025/05/image-1748060834816.png" alt="image-1748060834816" /></p></li></ul><h3 id="%E5%90%88%E5%B9%B6%E7%9B%B4%E6%92%AD%E6%B5%81" tabindex="-1">合并直播流</h3><ul><li><p>有坑！！！<br />把上面的头文件和分片合并，这个对于流媒体小白来说太坑了，所有AI都回答用ffmpeg或者MP4Box之类工具，怎么尝试都不对，后来看来N_m3u8DL 项目源码，才知道原来只需要简单的用头文件流合并分片文件流 就行了，包括上面的MPD的分析都来自对N_m3u8D的项目调试结果，并不是权威或<a href="https://www.mpeg.org/standards/" target="_blank">MEPG公开标准</a>，感兴趣可自行研究。WebView2的 DevToolsProtocolEventReceived只能下载mpd中dash和m4s。ffmpeg 不能直接用分片转直播流的。</p></li><li><pre><code>c# 合并DAS头文件和分片的实现：</code></pre><pre><code class="language-">private void btnMerge_Click(object sender, EventArgs e)   {       string[] files = Directory.GetFiles(audioFolder, &quot;*.m4s&quot;);       List&lt;string&gt; listFiles = new List&lt;string&gt;();       string initFile = Path.Combine(audioFolder, &quot;audio=96000.mp4&quot;);       listFiles.AddRange(files);       Array.Sort(files);  // 确保文件按顺序排列   string outputFilePath = Path.Combine(audioFolder, &quot;merged_output.mp4&quot;);   using (Stream fileOutputStream = File.Open(outputFilePath, FileMode.Create, FileAccess.Write))   {        using (var inputStream = File.OpenRead(initFile))       {           inputStream.CopyTo(fileOutputStream);       }        foreach (var inputFilePath in files)       {           using (var inputStream = File.OpenRead(inputFilePath))           {               inputStream.CopyTo(fileOutputStream);           }       }    }</code></pre></li></ul><h3 id="%E4%BD%BF%E7%94%A8whisper-%E7%94%9F%E6%88%90%E5%AD%97%E5%B9%95" tabindex="-1">使用whisper 生成字幕</h3><ul><li><p>准备本地模型运行 (P15V笔记本电脑T600 4g  驱动cuda12.9) <a href="https://pytorch.org/get-started/locally/" target="_blank">pytorch</a> 环境：<br />pip install git+https://github.com/openai/whisper.git<br />pip install torch torchvision torchaudio --index-url <a href="https://download.pytorch.org/whl/cu128" target="_blank">https://download.pytorch.org/whl/cu128</a></p><pre><code class="language-">  C:\Users\chenl&gt;python Python 3.11.2 (tags/v3.11.2:878ead1, Feb  7 2023, 16:38:35) [MSC v.1934 64 bit (AMD64)] on win32 Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information. &gt;&gt;&gt; import torch &gt;&gt;&gt; print(torch.cuda.is_available()) True &gt;&gt;&gt; print(torch.cuda.get_device_name(0)) NVIDIA T600 Laptop GPU &gt;&gt;&gt;</code></pre></li><li><p>whisper_transcribe.py</p><pre><code class="language-">import whisper import sys def transcribe_audio(audio_path):     model = whisper.load_model(&quot;base&quot;)  # 使用最小模型（&quot;tiny&quot; 也可以，取决于你硬件配置）     result = model.transcribe(audio_path)     return result[&#39;text&#39;] if __name__ == &quot;__main__&quot;:     if len(sys.argv) &lt; 2:         print(&quot;Please provide an audio file path.&quot;)         sys.exit(1)     audio_path = sys.argv[1]     transcript = transcribe_audio(audio_path)     print(transcript)</code></pre></li><li><p>生成STT，用c# 调用python</p><pre><code class="language-">private void btnSTT_Click(object sender, EventArgs e){    string outputFilePath = Path.Combine(audioFolder, &quot;merged_output.mp4&quot;);    // 检查文件是否存在    if (!File.Exists(outputFilePath))    {        MessageBox.Show(&quot;音频文件未找到！&quot;);        return;    }    // 调用 Python 脚本进行音频转文字     string sourceDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);     string whisperScriptPath = Path.Combine(sourceDirectory, &quot;whisper_transcribe.py&quot;);    string pythonExePath = &quot;python&quot;; //python 在环境变量已配                 ProcessStartInfo startInfo = new ProcessStartInfo    {        FileName = pythonExePath,        Arguments = $&quot;\&quot;{whisperScriptPath}\&quot; \&quot;{outputFilePath}\&quot;&quot;,  // 传递音频文件路径        RedirectStandardOutput = true,        UseShellExecute = false,        CreateNoWindow = true    };    Process process = new Process    {        StartInfo = startInfo    };    try    {        process.Start();        // 获取 Python 脚本输出（转录的文本）        string output = process.StandardOutput.ReadToEnd();        process.WaitForExit();        // 弹出新窗体并显示转录结果        TranscriptionForm.ShowTranscription(output);        // 可选：将转录结果保存到文件中        string outputTextFilePath = Path.Combine(audioFolder, &quot;transcription.txt&quot;);        File.WriteAllText(outputTextFilePath, output);    }    catch (Exception ex)    {        MessageBox.Show($&quot;错误: {ex.Message}&quot;);    }}</code></pre><p><img src="/upload/2025/05/image-1748085088874.png" alt="image-1748085088874" /></p></li></ul>]]>
                </content>
            </entry>
</feed>
