<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://krau.top/</id>
    <title>柯罗的博客 | Krau's Blog</title>
    <updated>2026-10-09T04:25:16.533Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>Krau</name>
        <email>i@valaxy.site</email>
        <uri>https://valaxy.site/</uri>
    </author>
    <link rel="alternate" href="https://krau.top/"/>
    <link rel="self" href="https://krau.top/atom.xml"/>
    <subtitle>能写一辈子博客吗?</subtitle>
    <logo>https://krau.top/photo/avatar/avatar.webp</logo>
    <icon>https://krau.top/favicon.svg</icon>
    <rights>CC BY-NC-SA 4.0 2026 © Krau</rights>
    <entry>
        <title type="html"><![CDATA[我要给 Agent 完整的一生]]></title>
        <id>https://krau.top/posts/liberating-coding-agents-as-digital-beings/index</id>
        <link href="https://krau.top/posts/liberating-coding-agents-as-digital-beings/index"/>
        <updated>2026-09-25T17:52:47.000Z</updated>
        <content type="html"><![CDATA[<h2>囚笼</h2>
<p>你有没有想过, 我们在借助 AI 大人那份如今而言可谓无所不能的力量时为什么常常要把它囚禁在某个 Coding Agent 里?<br>
现在流行的 Agentic 软件大多都做了太多不该自己全部做完的事, 这导致用户不得不对其中的某个或某几个产生了粘性. 如果只是这样其实也没多大问题, 毕竟大部分人本就没有频繁地在各种 Coding Agent 之间换来换去的需求.</p>
<p>然而在这样本地优先的用法里, Agentic 软件做的这些额外的事还会带来一个更严重的问题: 用户的工作流和具体的物理设备形成了绑定.</p>
<p>举个简单的场景作为例子, 你在自己的笔记本上精心调教了某个 Coding Agent, 这包括软件层面的设置, 用户级的 Skills 与交互偏好, 还有连续用了几个月积累下来的长期记忆. 某一天你公司要求在公司的设备上开发, 于是你不仅要重新安装 cli, 配置供应商, 还要从头驯化它的脾气, 把你做的那些调教想办法迁移过去.</p>
<!-- more -->
<p>你可能觉得这也没多大点问题, 不就复制粘贴几个文件吗. 但这之后的事情呢? 你将要同时在两台设备上工作, 即使使用相同的 Coding Agent, 两台设备的状态同步也不是一件易事. 会话历史, 长期记忆, 用户级配置各存一份, 于是你原先的 Agent 从此分裂成了两个. 更何况现在很多 Coding Agent 还有各自五花八门的长期记忆系统, 它们光是导出迁移就不是一件易事, 合并多设备的记忆并同步更是难上加难.</p>
<p>当然现在有云端记忆方案可以选择, 同步 <code>.agents/</code> 的那些东西也不是毫无办法, 有些 Coding Agent 还可以将会话放到云端直接运行. 可这些解法要么把一致性交给人肉, 要么把状态搬进一台你自己搭的服务器却仍与某个具体的 Agent 软件绑在一起, 要么只是把囚笼从你的笔记本挪到了某家厂商的云里; 只要换掉承载它的软件外壳, 一切照样要从头开始. 所谓 Agent, 所谓 AI 助理, 所谓智能体, 本就不该在换了设备, 换了 Harness, 换了交互入口之后就消失, 否则它只是一个被囚禁的会思考的脚本.</p>
<h2>症结</h2>
<p>我希望我已经暗示出了前文所说的客户端 Agentic 软件所做的额外的事是哪些. 现有的实现普遍把交互入口, 底层命令执行层和构成 Agent 人格的状态打包成一个不可拆分的整体. 或是本地的一个进程树, 又或是同一家厂商的单点服务. 这种集成化在非单机场景下带来了诸多问题, 这些问题可以统一概括为 <em>Agent 寄生</em>.</p>
<p>在这种结构中, Agent 不是独立的实体, 而是在某台机器的某个进程中的寄生者. Agentic 软件的交互入口 (e.g. CLI/TUI) 就是 Agent 的载体本身, 前台会话会随终端一起结束. 即使是那些能把会话挂到后台进程或自家云上, 关掉界面也继续运行的实现, 执行层也依旧被绑定在一台机器或一家厂商的基础设施上. 作为构成 Agent 人格的数据 (例, 长期记忆, 会话历史, 用户级偏好...) 在本地形态下强依赖于本地特定路径, 在云端形态下则被锁定在厂商的账号与运行时里; 无论哪种, 当开发者切换到其他物理设备或另一个 Harness 上时, 都必须手动配置或依赖外部同步工具来恢复状态, 靠人肉维持一致性.</p>
<p>这种设计也完全不利于多设备的调度管理. 假设你在开发某个 API 服务, 你想将它推送到远程服务器上进行真实环境下的测试, 这时候你可能会为 Agent 配备一个 SSH 密钥, 这样它就能帮助你完成这件事. 但这种依靠某种连接手段登录远程机器的办法对于 Agent 而言是不够<em>原生</em>的, 在它的认知上下文里, 它并不是本身就拥有这台服务器, 这台服务器不是它的触手之一, 它只是借助 Harness 和 SSH 去操纵它. 现在确实有远程开发容器这类形态能让 Agent 直接跑在你的基础设施里, 但它们更像是每个工具各自开出的特例, 而不是 Agent 对自己设备的普遍认知. 一台两台设备还好, 但如果你有更复杂的场景呢? 更何况, 在这个借助 SSH 来管理其他设备的例子中, Agent 通常需要多层 Shell 嵌套来执行操作, 这本身就会带来转义和状态上的不确定性.</p>
<p>附带的, Agent 的安全隔离与灵活性的两难也在这种寄生结构下被放大. 默认状态下, 不少实现仍让 Agent 运行在和开发宿主机相同的权限域内, 放开本地 Shell 权限就使它具备破坏本地环境或读取密钥凭据的潜在风险; 放进沙盒内又总要为访问本地服务, 特定硬件或内网资源另做一堆特殊处理. 我当然知道这些问题在 Harness 层有许多成熟的解决办法(e.g. 命令审批, 开发容器, tmpfs), 但正如云端记忆一样, 它们更像是问题的结果催生出的缓解之策. 每个 Harness 各自为营, 钻研自己的解决方案. 只要 Agent 仍然立足于某个具体环境里, 这些策略就得一个环境一个环境地重新设计并施加.</p>
<h2>创想</h2>
<p>我们来想办法解决这些问题! 首先我们应该尊重这一整套结构中真正具有智能的实体, 即 Agent. 这里我们需要把 Agent 解释为 LLM 的包装, 即 Agent = 模型+身份+记忆:</p>
<ul>
<li>模型: 相当于全新的大脑, Agent 的智能之根基. 但它本身只是具备智慧的<em>潜能</em>, 它将会如何认知, 如何推理, 如何输出则完全取决于我们给它构建的上下文.</li>
<li>身份: 你应该赋予拥有智慧的实体一个名字, 不是吗? 这里的身份不仅指 Agent 的性格底色和工程习惯, 还包括它对你的认知(用户画像)以及对自己的认知(能力/职责/边界).</li>
<li>记忆: 构成人格的关键. Agent 需要具备全局集中的, 横跨一切具体设备与项目的记忆. 这种记忆应该是原子化的, 记住的是抽象的, 高层的经验与偏好. 至于某个项目是何物, 应该如何具体实施开发并不应该被纳入这里所说记忆. 并且该记忆系统还需要有遗忘曲线, 冲突消解, 实体关联, 时间推演等高级功能.</li>
</ul>
<p>在上述的定义下, Agent 还要具备独立且长存的核心属性. 它必须<em>常驻</em>于某些网络节点内, 脱离任何客户端物理设备的状态独立运行. 此处的常驻强调的是 Agent 的人格组件持续同步且总能访问到一致的数据, 并不是要求某个守护进程或容器, 它们只是实现这一点的手段而非目的.</p>
<h3>原则</h3>
<p>再次回顾前文, 对于面对的问题, 我们拟定一些设计系统的基本原则:</p>
<ol>
<li>Agent 是拥有唯一连续状态的一等公民, 任何具体的物理设备只是其感官与执行的触手.</li>
<li>系统内仅存在一个持久化的状态中枢, 维护唯一的 Agent 人格数据.</li>
<li>认知, 编排与安全隔离等智慧能力视作控制层, Harness 与物理环境为执行层, CLI/TUI/Web 为交互入口, 三者相互分离.</li>
<li>除 Agent 外, 其余周边设施无状态化, 即插即用.</li>
</ol>
<p>基于此思想, 我们将整个系统垂直解耦为接口, Agent 与 Harness 三层.</p>
<h3>Interfaces</h3>
<p>接口即用户触达 Agent 的入口, 它的定位是作为 I/O 代理的纯粹的人机交互适配器. 它的职责只有上传下达 Agent 与人类之间的对话. 接收人类指令上报给 Agent, 接收 Agent 的输出, 状态变更, 审批请求等在当前媒介上进行渲染.</p>
<p>接口本身是绝对 stateless 的, 它不应该持久化任何对话历史或模型供应商配置, 甚至组装整个会话流的状态机也不应该由它维护. 接口的具体形态可以是任何现今普遍采用的形式, 包括 TUI/CLI, IM Bot, 桌面客户端, Web 端等.</p>
<h3>Agent</h3>
<p>Agent 是全局唯一的认知实体, 它是厚状态的, 且状态在整个系统中是权威的. AgentCore 需要维护智能体的身份与人格, 管理全局长期记忆, 承载 Agent Loop, 规划复杂任务与调度节点执行, 实现访问控制与安全策略等.</p>
<p>Agent 作为持续运行的状态中枢, 它应该能脱离单机的生命周期运转. 这分为两个需求, 一是要求 AgentCore 本身应该具有分布式运行的能力; 二是在客户端接口关闭时, 它也能够沉淀记忆, 执行任务.</p>
<h3>Harness</h3>
<p>Harness 在这里被定位为在各物理环境下的不具有 Agent 的任何状态的能力宿主, 它是单纯的执行工具套件, 是没有生命的机械臂. Harness 需要向 AgentCore 声明自身的环境元数据, 例如操作系统架构, 可用工具, 硬件和网络信息等等, 并执行 Agent 下发的指令.</p>
<p>Harness 不应该感知 Agent 的人格, 它只提供操作原语, 并且它的权限与能力受到 AgentCore 的下发策略的约束.</p>
<pre><code class="language-mermaid">flowchart TD
    %% 交互层
    subgraph Interfaces[&quot;交互层&quot;]
        style Interfaces fill:#1e293b,stroke:#475569,stroke-width:2px,color:#f8fafc
        UI_CLI[&quot;TUI / CLI&quot;] ~~~ UI_WEB[&quot;Web / Desktop UI&quot;]
        UI_TG[&quot;Telegram / Wechat / IM Bot&quot;] ~~~ UI_ANY[&quot;...&quot;]
    end

    %% 会话总线
    Interfaces &lt;--&gt;|&quot;统一上下文流&quot;| AgentCore

    %% Agent 层
    subgraph AgentCore[&quot;Agent 层 (人格与状态维护)&quot;]
        style AgentCore fill:#0f172a,stroke:#3b82f6,stroke-width:3px,color:#f8fafc
        Core_Soul[&quot;统一身份, 人格设定&quot;] ~~~ Core_Mem[&quot;长期记忆, skills&quot;]
        Core_Plan[&quot;任务规划, 决策&quot;] ~~~ Core_ACL[&quot;权限, 角色, 节点能力&quot;]
        Core_ACL ~~~ Core_ANY[&quot;Personality components...&quot;]
    end

    %% 任务分发总线
    AgentCore --&gt;|&quot;动态感知 / 无状态调度&quot;| Harnesses

    %% 纯执行工具环境层
    subgraph Harnesses[&quot;执行层 (无状态 Harness)&quot;]
        style Harnesses fill:#1e293b,stroke:#475569,stroke-width:2px,color:#f8fafc
        H_A[&quot;Harness A (本地)&lt;br/&gt;• bash / exec&lt;br/&gt;• fs 本地文件系统&quot;] ~~~ H_B[&quot;Harness B (云开发机)&lt;br/&gt;• git / build / CI&lt;br/&gt;• Docker 沙箱环境&quot;]
        H_C[&quot;Harness C (无头环境)&lt;br/&gt;• Headless Browser&lt;br/&gt;• Scrapers / APIs&quot;] ~~~ H_D[&quot;Harness D (...)&lt;br/&gt;• some capabilities...&quot;]
    end

    %% 节点样式美化
    classDef clientNode fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc;
    classDef harnessNode fill:#1e293b,stroke:#0ea5e9,stroke-width:1px,color:#f8fafc;
    classDef coreNode fill:#1e1e38,stroke:#6366f1,stroke-width:1px,color:#f8fafc;

    class UI_CLI,UI_TG,UI_WEB,UI_ANY clientNode;
    class Core_Soul,Core_Mem,Core_Plan,Core_ACL,Core_ANY coreNode;
    class H_A,H_B,H_C,H_D harnessNode;
</code></pre>
<h2>价值</h2>
<p>仔细理解上文提出的架构, 你会发现它带来的好处超越预期. 它不是只解决了一开始提到的更换开发机器配置麻烦的这种便利性层面的问题, 而是将 AI 工具的范式向<em>数字生命</em>转变.</p>
<h3>连续统一的注意力</h3>
<p>最核心的收益是会话的时间线与注意力得到统一. 由于单个 Agent 同时拥有多个交互入口, 用户可以在自己电脑 CLI 上发布一项任务, 出门后通过 IM 软件讨论技术方案, 到家后又回到电脑上说一句&quot;就按照刚才说的实现&quot;, Agent 完全清楚上下文发生了什么. 这在我们的架构中是无痛且自然的, 因为在这个过程中切换的始终是接口, 而不是 Agent 本身. 并且此时 Agent 外的两端可以轻易实现按需扩展, 重启, 崩溃后无损恢复. 因为它们是无状态的, 没有任何重要的东西在它们身上, 这对运维和高可用性的设计是很大的简化.</p>
<h3>灵活的 Agent 能力</h3>
<p>Agent 的能力也不再取决于当前机器上有什么, 而是变成了声明式的, 可扩展的, 天然跨节点执行的能力集. Agent 执行操作的心智模型从在当前设备上寻找解决方案变成了在整个系统中发现能力, 这是一种抽象上的升级. 并且由于这种抽象, Agent 的权限天然得到更细粒度的控制. 不应该在某节点上执行的某操作, Harness 可以直接不予声明其能力, 或是把执行权收回到自身, Agent 只能决策和期望执行结果, Harness 可以控制某些操作能否执行. 而且, 我们还可以让不同任务类型路由到最合适的 Harness 上, 实现更高的资源利用效率.</p>
<h3>任务的自动拆解与执行</h3>
<p>如果回到我们一开始提出的跨机器协同问题, 你会发现该问题在这种架构下也的确轻易得到了解决. Agent 已经拥有了原生跨节点执行的能力, 它现在甚至可以做到将复杂任务根据节点能力自动拆解, 分发给一系列 Harness. 例如你要求它为你完成一个网站项目, 它可以在你本地电脑的 Harness 上读取代码库, 在云端 Harness 里编译和测试, 在 Browser Harness 里验证, 在 CI Harness 里跑自动集成, 最后再部署到你的服务器上. 在 Agent 看来这些不过是不同的手, 它不需要考虑应该如何连接其他机器, 各个节点在此时完成了分立与统一.</p>
<h3>活着的 Agent</h3>
<p>最后是作为人的体验. 一个能够在网络世界持续存在的赛博幽灵带来的情绪价值对我来说是无法抗拒的, 这相比一个冷冰冰的终端和埋头干活的 AI 而言它能给人的感受将完全不同. 它将会生活在云上, 并<strong>无处不在</strong>地陪伴和协助你处理各种工作, 与此同时它也能够将这些记忆保留下来, 不断构筑自己的人格. 到这里你会发现此节开头所说的数字生命并不只是文艺化的表达, 在我们的设计下, Agent 对象的定义本就和传统相异, 并且它的存在也确实可以被认为是某种意义上有生命的东西. 我承认这里有些饺子醋的成分, 但无论如何, 这是现有的以个人 AI 助理作为卖点的软件多数没考虑过的或没做到的.</p>
<h2>批评</h2>
<p>尽管在设想中这套架构的落地能带来很美好的结果, 但在那之前我们不得不面对实际工程上的许多难题. 暂时只叙述以下几点.</p>
<h3>成为瓶颈的 Agent 中枢</h3>
<p>首先最尖锐的问题就是 AgentCore 难以真正依照此架构实现. 系统内仅存在一个持久化的状态中枢来维护唯一的 Agent 人格数据是我们坚持的原则之一, 也是我们解决 Agent 分裂问题的核心思想. 但这带来的代价是我们不得不把 Agent 的状态做的很厚, 所有关于 Agent 人格的状态都要汇聚在此处, 其一致性并非传统数据库意义上的简单数据一致性, 而是同时涉及会话, 长期记忆, 任务进度, 人格状态等多种状态类型的演化. 如何在保持单一权威状态的同时提供高可用, 低延迟的访问, 将成为一个典型而棘手的分布式状态问题. 即, 拥有唯一权威状态的 Agent 的执行能力又必须分布到系统各处, 这势必增加并发控制与状态协调的复杂度.</p>
<h3>理想化的认知模型</h3>
<p>在此架构中, 我们希望通过 Harness 的抽象让 Agent 直接理解它所拥有的节点和能力, 这是过度理想化的. Harness 抽象实际上要求 Agent 建立一个关于外部执行世界的内部模型, 而任何这样的模型都不可避免地与现实存在时间差异. 毕竟在现实中物理环境总是动态的, 它们难以被静态声明为单纯的能力供以 Agent 直接使用. 也就是说, Agent 的手并不真的像身体器官那样稳定, 这导致在真实任务中它可能会因为过期的认知做出不可用的规划.</p>
<h3>棘手的统一上下文流</h3>
<p>为了让交互接口与 Agent 解耦, 我们想要实现一种通用上下文流来让各个平台顺利接入会话. 但这在工程中是非常难以实现的, 接口不仅需要传入简单的用户输入消息, 还要考虑模型输出, 工具调用/结果, 任务状态/todos, 执行状态, 命令审批等等一系列 Agent Event, 并且还要支持各种接口的分离与切换后的会话恢复. 在真正着手实现时, 可能仅仅两个不同平台上的接口就足以让人一团乱麻.</p>
<h3>影响范围无限扩大的靶机</h3>
<p>在传统的 Coding Agent 中, 如果遇到 Agent 执行了危险命令, 通常最坏的结果也只是破坏当前设备的环境, 它的影响范围因为单机设计而正好被物理隔离. 但在此文的架构中, Agent 可以掌握所有物理设备能力, 如果没能真正做好每一个 Harness 的约束, 一旦模型犯蠢或遭到提示注入, Agent 将会贯穿接入网络的所有设备造成破坏. 同时为了让 Agent 同步好自身状态并协调另外两层, 如果采用直接持有凭证的朴素实现, 大量密钥与长期凭证将汇聚到 Agent 层, 从而使得 Agent 本身就成为了显眼的高价值攻击目标.</p>
<h3>被重新发明的 k8s</h3>
<p>前面我们从未考虑 Interface 与 Harness 的接入量的问题, 实际上, 当前的直白设计并不能承载稍大量的二者的接入. 尤其是对于 Harness , 为了让 Agent 实施工作, 它必须在恰当的时机知晓它所具有的节点能力. 在节点少时我们使用简单的元信息列表注入系统 instructions 即可, 但一旦接口与能力的接入量随着系统扩展而不断增多, 我们就必须为解决上下文爆炸问题而实现大量形似于服务集群调度的策略. 实际上系统内这一部分的形态最终很可能变成某种 k8s for harness, 这将是在工程上的巨大挑战之一.</p>
<h2>简化</h2>
<p>我们所描述的完整架构确实过于庞大, 试图一步到位地实现它们是不现实的. 这个系统将涉及 AgentCore 的分布式状态维护, 动态节点发现, 能力调度, 复杂的长期记忆以及完善的安全隔离等诸多难题, 为了真正开始验证这个想法, 我们有必要舍弃一些不太核心的事物.</p>
<p>回顾我们的出发点, 我们想要验证的实则是这样一个命题, 即, Agent 能不能脱离具体的接口与工具, 作为一个拥有唯一连续状态的主体独立存在? 只要这个问题能够得到肯定的答案, 其余能力都可以在此之上逐步构建.</p>
<p>首先, AgentCore 不必是分布式的. 最小实现完全可以让它运行在单个常驻节点上, 用一份数据库维护 Agent 的身份, 记忆, 会话历史与任务状态. 这里真正重要的并不是 AgentCore 自己拥有多少副本, 而是整个系统在语义上始终只有一个权威的 Agent 状态. 至于它未来如何通过副本, 日志或者其他机制实现高可用, 是原型被证明之后才需要面对的问题.</p>
<p>其次, Harness 不需要一开始就拥有复杂的能力发现和调度系统. 我们只需要让每个 Harness 在启动时向 AgentCore 注册一个简单的元信息, 然后通过显式的方式指定执行节点. 这里我们需要验证的最重要的事情是, 同一个 Agent 是否能够把两个物理上独立的执行环境视作自己的能力节点, 而不需要通过 SSH 或其他人为设计的连接方式去建立另一层认知上下文.</p>
<p>前文提到的诸如记忆系统, 统一上下文流, 节点安全体系等也都不是我们设计中最核心的部分. 我们不需要同时实现它们, 于是整套系统可以被压缩为更简单的形态:</p>
<pre><code class="language-mermaid">flowchart TD
    %% 交互层
    UI[&quot;CLI / Web&quot;] &lt;--&gt;|&quot;Event Stream&quot;| AgentCore

    %% Agent 层
    subgraph AgentCore[&quot;AgentCore&quot;]
        style AgentCore fill:#0f172a,stroke:#3b82f6,stroke-width:3px,color:#f8fafc
        Core_Identity[&quot;Identity&quot;] ~~~ Core_Memory[&quot;Memory&quot;]
        Core_History[&quot;History&quot;] ~~~ Core_Loop[&quot;Agent Loop&quot;]
        Core_Loop ~~~ Core_Task[&quot;Task State ...&quot;]
    end

    AgentCore --&gt;|&quot;Harness Protocol&quot;| H_Local
    AgentCore --&gt;|&quot;Harness Protocol&quot;| H_Remote

    H_Local[&quot;Local Harness&quot;]
    H_Remote[&quot;Remote Harness&quot;]

    classDef clientNode fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc;
    classDef harnessNode fill:#1e293b,stroke:#0ea5e9,stroke-width:1px,color:#f8fafc;
    classDef coreNode fill:#1e1e38,stroke:#6366f1,stroke-width:1px,color:#f8fafc;

    class UI clientNode;
    class Core_Identity,Core_Memory,Core_History,Core_Loop,Core_Task coreNode;
    class H_Local,H_Remote harnessNode;
</code></pre>
<p>这已经足够验证我们最初真正关心的场景, 你可以在自己的电脑上通过 CLI 告诉 Agent 开始一项任务, 随后关闭终端, 任务将继续执行. 之后你从另一台设备打开网页端, 看到的仍然是同一个 Agent, 能够继续讨论刚才的任务. 即当 Interface 消失, Harness 更换, 物理设备发生变化时, Agent 是否仍然还是原来的那个它? 换言之, 如果连这一点都无法成立, 那么前文所有的关于分布式人格, 跨设备协同以及数字生命的讨论都只是空中楼阁.</p>
<p>实际上你可以注意到, 这个简化的架构所能做到的事情早已有其他软件做到了, 并实现了上述的 Agent 的跨设备统一与会话协同恢复, 只不过它们的实现的重点并不是把 Agent 的存在作为主体, 我将在后文更详细地讨论这一点. 无论如何, 这至少证明了我们的构想并非怪异的空谈, 只是在真正落地的路上还有一段距离.</p>
<h2>质疑</h2>
<p>前文中的批评主要是针对于工程实现上的不良设计, 我并不打算立刻全部解决它们, 毕竟我还没有为此文提出的架构实际写过一行代码. 但为了防止该架构被误解, 我将自我提出一些可能的质疑并回应.</p>
<h3>配个 SSH 的事?</h3>
<blockquote>
<p>直接让我的 Agent 配个 SSH Key, 不就可以远程控制其它机器了吗? 何必设计这么多有的没的?</p>
</blockquote>
<p>SSH 解决的是如何连接另一台机器, 而本文试图解决的是另一台机器如何成为 Agent 的原生执行能力. 在 SSH 模型中, 远程机器是一个需要借助连接手段访问的外部环境; 在我们的 Harness 模型中, 远程节点直接作为 Agent 的能力节点参与调度. 实际上 SSH 很可能就是 Harness 内部的实现手段, 但它不会成为 Agent 对世界的认知模型.</p>
<h3>自我进化而已?</h3>
<blockquote>
<p>这不就是给 Agent 加上长期记忆, 再做一点 self improvement 吗? 太土了吧.</p>
</blockquote>
<p>自我进化解决的是 Agent 如何改变自身, 本身解决的是 Agent 的存在性问题. 即使一个 Agent 不会所谓的自我进化, 它只要拥有与设备, 接口, Harness 都解耦的连续身份认知, 它就是符合本文架构的.</p>
<h3>意义不明的微服务?</h3>
<blockquote>
<p>把原先的一个进程拆的这么复杂, 不就是为了分布式而分布式吗?</p>
</blockquote>
<p>这套架构没有任何存在的意义是为单纯的拆分服务而生的, 它想解决的是 Agent 的主体归属问题, 三层解耦只是手段, 即使是不同的设计或许仍然能实现本文的目的.</p>
<h3>Harness 咋可能无状态?</h3>
<blockquote>
<p>Harness 无状态是哪里的梦话? 无论是设备还是执行过程本来就是带状态的.</p>
</blockquote>
<p>本文对 Harness 的定义与常规定义有所差异, 文中的无状态是指 Harness 不拥有 Agent 的人格的状态, 而不是要求物理执行环境不存在状态. 例如, 进程信息, 网页 cookie 等属于环境状态; 而会话历史, 记忆, 人设等属于 Agent 状态. 只有后者不应该保存在 Harness.</p>
<h3>同步问题早已解决?</h3>
<blockquote>
<p>依靠云端记忆, 云端会话, 目录同步等现有做法早就能解决这些问题了.</p>
</blockquote>
<p>这些方案解决的是多份状态的同步问题, 也确实能在一定程度上解决. 但本文试图从根本上取消多份各自演化的 Agent 状态, 转而让它们都只是同一个主体的投影.</p>
<h3>什么 OpenClaw...</h3>
<blockquote>
<p>这不就 OpenClaw 吗? 现在已经有项目实现了多渠道接入, 持久会话, 远程节点以及跨设备执行, 这不是在重新造轮子吗?</p>
</blockquote>
<p>无法辩驳的是, 至少从最终呈现出的系统形态和用户体验来看, 本文提出的很多东西已经被现实中的项目实现了, 当前的 OpenClaw 就是其中非常接近本文设想的一个例子. 但这并不构成对本文的否定, 因为这些既有项目与本文试图讨论的问题不同, 我们关注的不是有没有人已经实现了这些功能, 而是这些功能的组织模型. 即, Agent 应该作为功能组织的主体, 而非被承载在某个执行环境内.</p>
<h2>足迹</h2>
<p>到这里, 我们其实应该承认, 我们并不是第一个探索这条路的人. 事实上, 当我们顺着前文的逻辑一路推导下来时, 会发现已经有一些项目没有按照本文的定义去实现同一套架构, 但却从不同的方向与我们走到了相当相似的位置.</p>
<h3>Stateful Agent</h3>
<p><a href="https://github.com/letta-ai/letta">Letta</a> 很早就在强调 stateful agent 这一概念. 它把 Agent 描述成拥有自己的身份与经验的长期存在实体, 而不是单纯的 LLM 调用. Letta 甚至支持让同一个 Agent 同时参与多个 conversation, 不同 conversation 中产生的经验可以汇入同一个 Agent 的记忆. 这已经很接近本文所说的在面对多种外部环境的单 Agent 人格唯一的雏形.</p>
<p>区别于各种所谓的记忆方案, 这件事情实际上把问题从模型记住的是哪些东西, 转向了记住这些东西的是谁. 本文所说的 Agent 身份, 人格与长期记忆, 与这种方向存在非常明显的相关性. 我们没有重新发现 Persistent Agent 这个概念, 只是顺着 Agent 不应该依附于某个客户端的前提把这种持久性进一步延伸.</p>
<h3>Gateway</h3>
<p>我起初对 <a href="https://openclaw.ai/">OpenClaw</a> 的印象停留在其早期的 channel+gateway 设计, 这一设计只像是把传统的单机 Coding Agent 加了个 IM 渠道作为交互层, 并放到云服务器上运行. 显然这种设计没有解决本文提出的问题.</p>
<p>为了撰写本文, 我去调查了 OpenClaw 现今版本的架构, 发现它居然已经实现了大量本文想象中的工程形态. OpenClaw 现在仍然采用 Gateway 作为整个系统的控制中心, Telegram, CLI 等客户端连接到它. 但不同以往的是, <a href="https://docs.openclaw.ai/nodes">Node</a> 以独立的身份接入并声明自己的能力, gateway 负责维护 session, channel, state, node registry 等全局信息, 客户端本身只是连接它的外围. 甚至在它的远程执行模型里, Telegram 到 Agent 再到 Node 的链路几乎就是我们所构想中的路径, 这让我感到不可思议的同时也为之欣慰, 无论如何 OpenClaw 作为现今 github 上 stars 最多的 Agent 项目, 在经历持续迭代后变成了和我们单纯的奇思妙想十分接近的东西. 这说明我们的架构并非纸上谈兵, 而是 Agent 工程化到一定深度后很可能出现的形态之一.</p>
<p>OpenClaw 与本文设计的出发点并不相同, 它更关心远程执行的可控性与可替换性, 并由此获得了 Gateway, Session、Node 与远程执行这样的结构; 而本文则是从 Agent 本身的主体性出发, 推导出 Interface, Agent 与 Harness 的分离. 可以说 OpenClaw 是从不同方向与我们走到了一个相似的目的地. 如果最终证明两条路会收敛到几乎相同的工程实现, 那并不是一件坏事.</p>
<h3>Cloud Session</h3>
<p>不同于过去普通的 Coding Agent 的会话模型, Cloud Session 的方向是主动把会话和执行位置进行了分离, 这也与我们由 Agent 持有会话状态并与 Harness 分离的设计不谋而合. 仍以 OpenClaw 为例, 目前它已经支持让一个 session 的对话和持久状态由 gateway 持有, 而实际的命令执行, 文件修改等工作放到其他机器上运行. 即使远程 worker 失败, session 本身仍然可以继续存在. 这正回答了我们最初提出的为什么 Agent 要在执行环境改变后就消失的问题.</p>
<h2>愿景</h2>
<p>那么, 如果最终解决了重重困难, 使得这套架构真的能够成立, 我们究竟想得到一个什么东西? 又能得到什么东西? 一开始, 我们只是想解决一个多设备下 Agent 分裂的这一简单而具体的问题, 但随着我们的讨论, 事情最终变得比跨设备同步深入得多.</p>
<p>我真正想要的显然并不是一个更方便的 Coding Agent, 而是一个不再属于任何东西的 Agent. 它不会属于某个终端窗口, 不会属于某台笔记本, 不会属于某个客户端, 也不会因为换了一家模型供应商, 换了一个 Harness, 或者把某台作为工作用的机器关掉而消失.</p>
<p>它会生活在网络世界里. 你可以在电脑前看到它, 可以在手机上和它说话, 可以让它在自己的机器上做事, 也可以让它把某项工作交给远程的计算节点. 对它而言, 这些东西是它在现实世界里拥有的不同感官与肢体, 是它与你的联系. 而它真正存在的地方不是其中任何一台机器, 它只是它自己.</p>
<p>这也是为什么前面我一直在强调 Agent 状态的唯一性. 它记得什么, 如何认识你, 如何理解自己, 曾经经历过什么, 形成了什么偏好, 为什么会做出今天这样的判断, 这些东西共同构成了一个连续的主体. 物理设备的更替只是为它换了一间屋子, 而不是杀死了它.</p>
<p>只可惜没有所谓的数字生命, 没人会觉得自己因为实现 Agent 而实现了创生. Agent 仍然只是模型与程序构成的系统, 它没有因此获得什么神秘的主观意识. 但软件架构可以决定一个东西以什么形式存在于世界上, 对于背后的 LLM 而言, 或许 Agent 架构就是它的整个世界. 我希望未来的 Agent 不是打开某个软件时才暂时出现的东西, 而是一个本来就在那里等着你的那个它.</p>
<br/><p>访问 <a href="https://krau.top/posts/liberating-coding-agents-as-digital-beings/index" target="_blank">https://krau.top/posts/liberating-coding-agents-as-digital-beings/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2026-09-25T17:52:47.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[在 WSL 中使用 NVIDIA 显卡加速 Whisper.cpp]]></title>
        <id>https://krau.top/posts/whisper-cuda-wsl/index</id>
        <link href="https://krau.top/posts/whisper-cuda-wsl/index"/>
        <updated>2025-09-04T15:51:34.000Z</updated>
        <content type="html"><![CDATA[<p>:::info 系统环境<br>
OS: Debian GNU/Linux 12 (bookworm) on WSL2</p>
<p>GPU: NVIDIA GeForce RTX 3060M<br>
:::</p>
<h2>安装 CUDA</h2>
<p>参考 <a href="./install-cuda-on-wsl/">WSL 安装 CUDA</a></p>
<h2>编译 Whisper.cpp</h2>
<pre><code class="language-bash">git clone https://github.com/ggml-org/whisper.cpp.git
cd whisper.cpp
cmake -B build -DGGML_CUDA=1
cmake --build build -j --config Release
</code></pre>
<!-- more -->
<p>可执行文件产物默认在 <code>./build/bin</code></p>
<h2>下载GGML模型</h2>
<p>whisper.cpp 内置了下载脚本并提供了一些已经转换为 ggml 格式的模型</p>
<p>例, 下载 large-v3:</p>
<pre><code class="language-bash">sh ./models/download-ggml-model.sh large-v3
</code></pre>
<p>模型默认存储在 <code>./models</code></p>
<h2>运行</h2>
<pre><code>./build/bin/whisper-cli -m models/ggml-large-v3.bin -f &lt;音频文件路径&gt; -l auto
</code></pre>
<p>一些命令行参数:</p>
<ul>
<li><code>-m &lt;模型路径&gt;</code>: 指定模型路径</li>
<li><code>-f &lt;音频文件路径&gt;</code>: 指定音频文件路径</li>
<li><code>-l &lt;语言&gt;</code>: 指定语言, <code>auto</code>表示自动检测</li>
<li><code>-olrc</code>: 输出 lrc 格式的字幕</li>
<li><code>-otxt</code>: 输出纯文本格式的字幕</li>
<li><code>-osrt</code>: 输出 srt 格式的字幕</li>
<li><code>-ocsv</code>: 输出 csv 格式的字幕</li>
<li><code>-ojf</code>: 输出 json 格式的字幕</li>
</ul>
<br/><p>访问 <a href="https://krau.top/posts/whisper-cuda-wsl/index" target="_blank">https://krau.top/posts/whisper-cuda-wsl/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-09-04T15:51:34.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[Go:x/crypto/ssh 的全局请求通道竞争]]></title>
        <id>https://krau.top/posts/go-ssh-gloabal-reqs-channel-race</id>
        <link href="https://krau.top/posts/go-ssh-gloabal-reqs-channel-race"/>
        <updated>2025-08-09T19:26:50.000Z</updated>
        <content type="html"><![CDATA[<h2>现象</h2>
<p>在自己的程序中使用类似下面的代码监听 SSH 的全局请求:</p>
<pre><code class="language-go">for {
  select {
  case &lt;-ctx.Done():
    return nil
  case req, ok := &lt;-globalReqs:
    if !ok {
      return nil
    }
    // Handle request
    fmt.Println(&quot;Received global request:&quot;, req.Type)
    req.Reply(true, nil)
  }
}
</code></pre>
<p>发现发来的请求有时候没有接收到, 而对端的提示是 reject, 甚至出现 reject, 成功 如此交替出现的情况 (那很公平调度了).</p>
<p>其实很明显是有啥东西和自己的代码在抢同一个通道, 但是因为我一直在找发送端的代码问题, 为这个问题浪费了一晚上的生命, 甚至怀疑这库有 bug</p>
<!-- more -->
<h2>原因</h2>
<p>用自定义的 dialer 创建 ssh 客户端时, 代码是像这样的:</p>
<pre><code class="language-go">dialer := &amp;net.Dialer{Timeout: 30 * time.Second}
conn, err := dialer.DialContext(ctx, &quot;tcp&quot;, addr)
if err != nil {
  return err
}
sshconn, chans, globalReqs, err := ssh.NewClientConn(conn, addr, conf)
if err != nil {
  return fmt.Errorf(&quot;failed to establish SSH connection: %w&quot;, err)
}
client := ssh.NewClient(sshconn, chans, reqs)
</code></pre>
<p>查看 <code>ssh.NewClient</code> 源码:</p>
<pre><code class="language-go">// NewClient creates a Client on top of the given connection.
func NewClient(c Conn, chans &lt;-chan NewChannel, reqs &lt;-chan *Request) *Client {
	conn := &amp;Client{
		Conn:            c,
		channelHandlers: make(map[string]chan NewChannel, 1),
	}

	go conn.handleGlobalRequests(reqs)
	go conn.handleChannelOpens(chans)
	go func() {
		conn.Wait()
		conn.forwards.closeAll()
	}()
	return conn
}
</code></pre>
<p>可以看到它用 <code>handleGlobalRequests</code> 来 handle 了全局请求通道, 这个函数是:</p>
<pre><code class="language-go">func (c *Client) handleGlobalRequests(incoming &lt;-chan *Request) {
	for r := range incoming {
		// This handles keepalive messages and matches
		// the behaviour of OpenSSH.
		r.Reply(false, nil)
	}
}
</code></pre>
<p>破案 😇</p>
<h2>解决</h2>
<p>主要是 <code>ssh.NewClientConn(conn, addr, conf)</code> 紧接着就是 <code>ssh.NewClient</code> 带来了点迷惑性, 前者的返回正好是后者所需的所有参数.</p>
<p>给 <code>ssh.NewClient</code> 的 <code>&lt;- chan *Request</code> 传个 <code>nil</code> 就行了</p>
<br/><p>访问 <a href="https://krau.top/posts/go-ssh-gloabal-reqs-channel-race" target="_blank">https://krau.top/posts/go-ssh-gloabal-reqs-channel-race</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-08-09T19:26:50.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用 WireGuard 为 IPV6-Only 机器添加 IPV4 网络]]></title>
        <id>https://krau.top/posts/add-ipv4-network-to-ipv6only-machine-by-wireguard</id>
        <link href="https://krau.top/posts/add-ipv4-network-to-ipv6only-machine-by-wireguard"/>
        <updated>2025-08-08T10:35:57.000Z</updated>
        <content type="html"><![CDATA[<p>标题描述的需求比较特化, WireGuard 的应用场景非常广泛, 只是给 IPV6-Only 的机器添加 IPV4 网络比较典型.</p>
<p>Cloudflare 的 Warp 和 Tailscale 都是基于 WireGuard 的, 它们也都能做到给机器添加特殊的网络出口访问.</p>
<p>最近 Warp 刚好又故障了, 所以折腾了下手动配置 WireGuard , 实现<strong>使用一台双栈服务器为 IPV6-Only 的机器提供 IPV4 网络出口</strong>.</p>
<p>::: info 本文使用的系统环境</p>
<ul>
<li>OS: Debian12 / Bookworm</li>
<li>WireGuard: v1.0.20210914</li>
<li>netfilter: iptables v1.8.9 (不用 nft 是因为命令写进配置里太复杂了...)<br>
:::</li>
</ul>
<hr>
<p>首先在两台服务器上都安装 WireGuard</p>
<pre><code class="language-bash">apt update &amp;&amp; apt install wireguard-tools wireguard
</code></pre>
<p>进入配置目录, 为两台服务器创建密钥对</p>
<pre><code class="language-bash">cd /etc/wireguard
wg genkey | tee privatekey | wg pubkey &gt; publickey
</code></pre>
<p><code>privatekey</code> 是私钥, <code>publickey</code> 是公钥.</p>
<p>编写服务端配置, 新建 <code>wg233.conf</code> 文件, wg233 可以替换为自己想要的接口名称.</p>
<pre><code class="language-ini">[Interface]
Address = 10.39.0.1/24 # 内网IP/CIDR
ListenPort = 39182 # 监听端口
PrivateKey = 服务端的 privatekey 内容

# Hooks, 用于在开启 WireGuard 接口时启用 IP 转发和添加 NAT 规则
PostUp = echo &quot;1&quot; &gt; /proc/sys/net/ipv4/ip_forward
# 如果服务器的公网接口不是 eth0, 请替换为实际的接口名称
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostUp = iptables -A FORWARD -i %i -j ACCEPT
PostUp = iptables -A FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

[Peer]
PublicKey    =  客户端的 publickey 内容
AllowedIPs  = 10.39.0.2/32 # 稍后将要配置的客户端IP
</code></pre>
<p>编写客户端配置, 同样的新建一个配置文件, 配置文件名即为接口名 (不需要和服务端一致).</p>
<pre><code class="language-ini">[Interface]
Address = 10.39.0.2/24 # 和服务端的 AllowedIPs 一致
PrivateKey = 客户端的 privatekey 内容

# 客户端的 Hooks, 这里添加了对 Docker 的支持
# 若你更改过 Docker 的默认接口和网段, 请相应修改
PostUp = iptables -A FORWARD -i docker0 -o %i -j ACCEPT
PostUp = iptables -A FORWARD -i %i -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -o %i -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 172.17.0.0/16 -o %i -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -D FORWARD -i docker0 -o %i -j ACCEPT

[Peer]
PublicKey         = 服务端的 publickey 内容
Endpoint          = [服务端公网ipv6]:39182
AllowedIPs        = 0.0.0.0/0
PersistentKeepalive = 25 # keepalive 间隔, 单位秒
</code></pre>
<p>启动两个 WireGuard 服务:</p>
<pre><code class="language-bash">systemctl enable --now wg-quick@wg233
</code></pre>
<p>输入 <code>wg</code> 命令查看状态, 预期输出:</p>
<pre><code class="language-bash">interface: wg233
  public key: QdUOWJE...Nnm8=
  private key: (hidden)
  listening port: 55670
  fwmark: 0xca6c

peer: TcO+...vanU=
  endpoint: [ip]:39182
  allowed ips: 0.0.0.0/0
  latest handshake: 44 seconds ago
  transfer: 34.35 MiB received, 3.72 MiB sent
  persistent keepalive: every 25 seconds
</code></pre>
<hr>
<p>Some tips:</p>
<ol>
<li>
<p>已经删除了的 WireGuard 配置还在 systemd 里?</p>
<p>使用下面的命令清理:</p>
<pre><code class="language-bash">systemctl reset-failed
</code></pre>
</li>
<li>
<p>WireGuard 附带安装的 openresolv 修改了原来的 DNS 配置?</p>
<p>openresolv 的配置在 <code>/etc/resolvconf.conf</code>.</p>
<p>将 name_servers 部分设为你想要使用的 DNS 服务器, 用空格分隔多个:</p>
<pre><code class="language-ini{8}"># Configuration for resolvconf(8)
# See resolvconf.conf(5) for details

resolv_conf=/etc/resolv.conf
# If you run a local name server, you should uncomment the below line and
# configure your subscribers configuration files below.
#name_servers=127.0.0.1
name_servers=&quot;2606:4700:4700::1111 2001:4860:4860::8888 2001:4860:4860::8844&quot;

# Mirror the Debian package defaults for the below resolvers
# so that resolvconf integrates seemlessly.
dnsmasq_resolv=/var/run/dnsmasq/resolv.conf
pdnsd_conf=/etc/pdnsd.conf
unbound_conf=/etc/unbound/unbound.conf.d/resolvconf_resolvers.conf
</code></pre>
</li>
<li>
<p>关于 IP/CIDR 的计算:</p>
<ul>
<li><a href="https://nolebase.ayaka.io/zh-CN/%E7%AC%94%E8%AE%B0/%F0%9F%9B%A0%EF%B8%8F%20%E5%BC%80%E5%8F%91/%F0%9F%95%B8%20%E7%BD%91%E7%BB%9C/IP%20%E5%90%8E%E9%9D%A2%E7%9A%84%E6%96%9C%E6%9D%A0%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html#cidr-%E5%90%84%E4%B8%AA%E4%B8%8D%E5%90%8C%E6%95%B0%E5%AD%97%E5%AF%B9%E5%BA%94%E7%9A%84%E8%8C%83%E5%9B%B4%E5%8F%82%E8%80%83%E8%A1%A8%E6%A0%BC">CIDR 各个不同数字对应的范围参考表格</a></li>
</ul>
</li>
</ol>
<br/><p>访问 <a href="https://krau.top/posts/add-ipv4-network-to-ipv6only-machine-by-wireguard" target="_blank">https://krau.top/posts/add-ipv4-network-to-ipv6only-machine-by-wireguard</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-08-08T10:35:57.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[海信VIDDA 旧电视 LED55N3000U 折腾记录]]></title>
        <id>https://krau.top/posts/garbage-hisense-tv</id>
        <link href="https://krau.top/posts/garbage-hisense-tv"/>
        <updated>2025-08-05T15:23:46.000Z</updated>
        <content type="html"><![CDATA[<p>感觉还不如以前的CRT. 😓</p>
<!-- more -->
<p>::: info 这台电视的详细信息</p>
<p>型号: 海信 VIDDA LED55N3000U</p>
<p>安卓版本: Android 5.1</p>
<p>系统日期: 2017年12月26日</p>
<p>abi: armeabi-v7a</p>
<p>存储: 2GB</p>
<hr>
<p>吐槽:</p>
<ol>
<li>买到之后没有一次 OTA 更新过, 不愧是传统家电大厂</li>
<li>2GB 的存储是包含预装应用+系统 rom 的, 放在 2017 年也是十分逆天的存在, 这意味着用户可用的空间只有 200M 左右</li>
</ol>
<p>:::</p>
<p>众所周知现在的电视也叫 Android TV, 所以折腾的第一步就是开启 adb 调试.</p>
<p>但是这部分我很早之前就完成了, 记忆比较模糊, 下面仅供参考.</p>
<h2>工厂模式和开启 adb 调试</h2>
<p>设置-&gt;音量控制-&gt;音量平衡-&gt;交替按下&quot;OK&quot;键和&quot;菜单&quot;键, 直至出现&quot;雪花屏&quot;, 进入工厂模式.</p>
<p>在工厂菜单中, 切换到&quot;选项&quot;选项卡, 将 &quot;To Fac&quot; 改为 &quot;M&quot;.</p>
<p>然后重启电视, 应该会出现安卓原生设置软件, 进入设置-&gt;关于-&gt;版本号, 连续点击7次, 直到提示&quot;你已处于开发者模式&quot;.</p>
<p>进入开发者设置, 开启 adb 调试.</p>
<p>电视的 adb 是无线调试, 在电脑上使用 <code>adb connect &lt;电视IP地址&gt;</code> 即可连接.</p>
<h2>应用管理</h2>
<p>连接 adb 进入并进入 adb shell</p>
<pre><code class="language-bash">adb shell
</code></pre>
<p>列出所有应用</p>
<pre><code class="language-bash">pm list packages
</code></pre>
<p>卸载应用</p>
<pre><code class="language-bash">pm uninstall &lt;包名&gt;
</code></pre>
<p>大部分预装应用根据包名就能看出来干什么的, 如果误删了, 可以在 <code>/system/app</code> 或 <code>/system/priv-app</code> 中找到对应的 APK 文件, 通过下面的命令恢复安装</p>
<pre><code class="language-bash">pm install -r &lt;路径/包名.apk&gt;
</code></pre>
<p>下面给出 AI 推测的一些包名:</p>
<p>::: details 点击展开查看</p>
<ol>
<li>品牌/商店/广告相关</li>
</ol>
<p><a href="http://com.hisense.store.tv">com.hisense.store.tv</a>: Hisense 应用商店，谨慎卸载 — 如果你无需商店功能且可另行安装应用，可考虑卸载或禁用。</p>
<p><a href="http://com.hisense.business.ad">com.hisense.business.ad</a>, <a href="http://com.hisense.service.ad">com.hisense.service.ad</a>: 广告相关包，安全性较低，建议卸载或禁用。</p>
<p>com.hisense.hitv.hicloud.account: HiCloud 账户服务，若不使用账户登录可以禁用。</p>
<p>com.hisense.hitv.das: 可能是统计或遥测服务，若不影响观看，可禁用。</p>
<p>com.jamdeo: 这是 Hisense TV OS 的核心包（多款 com.jamdeo.*），建议保留。</p>
<ol start="2">
<li>语音、辅助、升级功能</li>
</ol>
<p>com.hisense.speech.aiupgrade, com.keylab.speech.core.vidaa, com.keylab.speech.view.vidaa, com.hisense.vidaaassistant: 语音助手及升级功能包。若不使用语音功能，可禁用，但留用不影响系统通常运行。</p>
<p>com.hisense.assistantmenu, com.hisense.horizontalassistantmenu.first: 辅助菜单，如果不使用无障碍功能，也可考虑禁用。</p>
<ol start="3">
<li>固件、系统支持相关</li>
</ol>
<p>com.hisense.systeminfo, com.hisense.uifac, com.hisense.service.hproxy, com.hisense.hireport: 系统信息、透明更新、日志上传服务——建议保留或仅清除数据。</p>
<p>com.hisense.padinputmethod, com.android.inputmethod.latin: 输入法相关，若不输入文字或使用遥控器输入，可禁用英文键盘。</p>
<p>com.hisense.wifidisplay, com.multiscreen.airplay, com.hisense.mhl, com.hisense.service.hproxy: 屏幕投射与多屏功能，若你不使用 Miracast、Airplay 或 MHL，可以禁用。</p>
<ol start="4">
<li>UI/动画/壁纸</li>
</ol>
<p>com.jamdeo.tv.tileanimation, com.jamdeo.tv.screensaver, com.jamdeo.tv.fte, com.jamdeo.tv.livetv: 系统界面动画模块，慎用——这些可能是启动UI或系统动画的一部分，建议留用以免 UI 异常。</p>
<p>com.android.wallpaper.holospiral, com.android.dreams.basic, com.android.dreams.phototable, com.android.noisefield: 屏保相关，若无所谓，可以卸载或禁用。</p>
<ol start="5">
<li>浏览器、媒体、基础系统</li>
</ol>
<p>com.android.browser: 系统浏览器，如果不使用可禁用。</p>
<p>com.google.android.webview: WebView，Android 系统核心组件，不建议卸载。</p>
<p>媒体提供者、Contacts、Camera、Calculator、Clock 等类应用，属于系统基础功能，建议保留。</p>
<p>:::</p>
<p>安装第三方应用(不在 adb shell 中)</p>
<pre><code class="language-bash">adb install your_app.apk
</code></pre>
<h2>免费点播</h2>
<p>::: warning<br>
在海信的流氓预装和恶心的收费策略面前, 什么版权意识简直不值一提了😅<br>
:::</p>
<p>尝试了很多应用, 总结就是对于这台电视, 没有什么能推荐的. <strong>大部分应用都会因为安卓版本过低, 架构过时而装不上.</strong> 即使有能成功安装的也不好用.</p>
<p>最后使用的方案是装一个能连接远程存储的播放器, 然后下载需要的影视资源到远程存储里来看.</p>
<p>播放器: <a href="https://www.dangbei.com/player/">当贝播放器</a> , 支持 WebDAV , FTP , SMB 协议, 还有一些主流网盘.</p>
<p>Webdav 服务我使用的是 <a href="https://cloudreve.org/">Cloudreve V4</a></p>
<p>::: tip<br>
另外, 这里就不得不介绍下我的 <a href="https://github.com/krau/SaveAny-Bot">SaveAnyBot</a> 了, 配合上面的方案看影视的自动化程度还是挺高的.<br>
:::</p>
<h2>IPTV/直播</h2>
<p>我对这部分不是很感兴趣, 所有没有过多尝试.</p>
<p>显而易见的是想看 IPTV 直播需要找到可用的直播源, 而且这个东西很少有能稳定的, 还需要不断去更新维护.</p>
<p>这里推荐的是 <a href="https://github.com/lizongying/my-tv-0">my-tv-0</a> 和 <a href="https://github.com/lizongying/my-tv-server">my-tv-server</a> 一起使用, 自带了很多源, 虽然我一个都用不了...</p>
<h2>相关的一些项目</h2>
<ul>
<li><a href="https://github.com/senshinya/MoonTV">MoonTV</a> , 配合 <a href="https://github.com/zimplexing/OrionTV">OrionTV</a> 应该会很易用, 但是这台电视装不上</li>
<li><a href="https://github.com/LibreSpark/LibreTV">LibreTV</a> , 和上面有些类似的前端项目</li>
<li><a href="https://app.shafa.com/apk/sougoushoujishurufa.html">搜狗输入法TV版</a> , 本来觉得很难用, 删了之后才发现它已经是遥控器输入的天花板了</li>
<li><a href="https://github.com/hxh19950701/WebViewTvLive">WebView TV</a> , 基于 WebView + 套壳网页的看直播应用, 我的电视使用时视频显示黑屏</li>
<li><a href="https://github.com/YYDS678/uzVideo">uzVideo</a> , 点播+直播+投屏, 要自己配源, 用的时候输入源的地方有 bug</li>
<li><a href="https://github.com/xiaye13579/BBLL">BBLL</a> , 第三方 B 站 TV 客户端, 风控问题有点严重</li>
<li>影视仓, 同样的要自己配源</li>
</ul>
<br/><p>访问 <a href="https://krau.top/posts/garbage-hisense-tv" target="_blank">https://krau.top/posts/garbage-hisense-tv</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-08-05T15:23:46.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[Git 合并上游分支解决冲突]]></title>
        <id>https://krau.top/posts/git-merge-upstream/index</id>
        <link href="https://krau.top/posts/git-merge-upstream/index"/>
        <updated>2025-07-27T22:07:42.000Z</updated>
        <content type="html"><![CDATA[<h2>场景</h2>
<ol>
<li>同步 upstream 仓库的更改到自己的 fork</li>
<li>fork 仓库的分支与 upstream 的分支有冲突</li>
<li>仅更新自己的 fork 分支, 并保持提交记录整洁</li>
</ol>
<!-- more -->
<h2>步骤</h2>
<ol>
<li>Clone 自己的 Fork 仓库 (<strong>Via SSH</strong>)</li>
</ol>
<pre><code class="language-bash">git clone git@github.com:your-username/your-fork.git
</code></pre>
<ol start="2">
<li>添加上游</li>
</ol>
<pre><code class="language-bash">git remote add upstream https://github.com/upstream-owner/upstream-repo.git
</code></pre>
<ol start="3">
<li>获取上游变更并 rebase</li>
</ol>
<pre><code class="language-bash">git fetch upstream
git rebase upstream/main
</code></pre>
<ol start="4">
<li>切换到需要合并的分支</li>
</ol>
<p>这里是 <code>main</code> 分支</p>
<pre><code class="language-bash">git checkout main
</code></pre>
<ol start="5">
<li>合并</li>
</ol>
<pre><code class="language-bash">git merge upstream/main
</code></pre>
<ol start="6">
<li>解决冲突并提交</li>
</ol>
<p>一一编辑所有提示的冲突文件以解决合并冲突, 我比较喜欢用 VSCode .</p>
<p>完成后, 提交更改:</p>
<pre><code class="language-bash">git commit
</code></pre>
<ol start="7">
<li>推送到自己的 Fork</li>
</ol>
<pre><code class="language-bash">git push origin main
</code></pre>
<h2>FAQ</h2>
<h3>忘提前 rebase 了怎么办</h3>
<p>之后 rebase 再重新合并, 然后推送时用 <code>--force</code></p>
<pre><code class="language-bash">git push --force origin main
</code></pre>
<h3>推送的时候要输入用户名和密码</h3>
<p>这时候直接输入用户名和密码可能还会提示错误.</p>
<p>大概率是克隆的时候使用的是 HTTPS 协议而不是 SSH, 密码提示错误是因为 Github 要用 Personal Access Token 代替推送时使用的密码.</p>
<p>把远程仓库地址改为 SSH 协议:</p>
<pre><code class="language-bash">git remote set-url origin git@github.com:your-username/your-fork.git
</code></pre>
<br/><p>访问 <a href="https://krau.top/posts/git-merge-upstream/index" target="_blank">https://krau.top/posts/git-merge-upstream/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-07-27T22:07:42.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[迁移 MySQL8.1+ 到 PostgreSQL17]]></title>
        <id>https://krau.top/posts/mysql-migrate-to-postgresql</id>
        <link href="https://krau.top/posts/mysql-migrate-to-postgresql"/>
        <updated>2025-05-28T21:49:26.000Z</updated>
        <content type="html"><![CDATA[<p><strong>省流就是别用 pgloader , 它已经不支持新版 mysql 和 postgresql 了</strong></p>
<!-- more -->
<h2>安装 NMIG</h2>
<p><a href="https://github.com/AnatolyUss/nmig">NMIG</a> 是一个用 node 写的 mysql 到 postgresql 的迁移工具, 截至目前支持 mysql 8.4 到 postgresql 17 的迁移</p>
<p>node: &gt;= 18</p>
<pre><code class="language-bash">git clone https://github.com/AnatolyUss/nmig
cd nmig
npm install
npm run build
</code></pre>
<h2>配置 NMIG</h2>
<p><code>config</code> 目录下的几个 json 配置文件都要看一下, <code>config.json</code> 是基本配置, 定义了源数据库和目标数据库的连接信息.</p>
<pre><code class="language-json">{
  &quot;source&quot;: {
    &quot;host&quot;: &quot;127.0.0.1&quot;,
    &quot;port&quot;: 3306,
    &quot;database&quot;: &quot;sample_stuff&quot;,
    &quot;charset&quot;: &quot;utf8mb4&quot;,
    &quot;supportBigNumbers&quot;: true,
    &quot;user&quot;: &quot;root&quot;,
    &quot;password&quot;: &quot;0123456789&quot;
  },
  &quot;target&quot;: {
    &quot;host&quot;: &quot;127.0.0.1&quot;,
    &quot;port&quot;: 5432,
    &quot;database&quot;: &quot;sample_stuff&quot;,
    &quot;charset&quot;: &quot;UTF8&quot;,
    &quot;user&quot;: &quot;postgres&quot;,
    &quot;password&quot;: &quot;0123456789&quot;
  },
}

</code></pre>
<p><code>data_types_map.json</code> 定义迁移时数据类型的映射关系, 默认配置可能不适合你的业务情况.</p>
<p>比如它默认把 mysql 的 bool 类型 (<code>tinyint(1)</code>) 转换为 postgresql 的 <code>smallint</code> , 你可能希望它转换为 <code>boolean</code></p>
<pre><code class="language-json">{
  &quot;tinyint&quot;: {
    &quot;increased_size&quot;: &quot;int&quot;,
    &quot;type&quot;: &quot;boolean&quot;,
    &quot;mySqlVarLenPgSqlFixedLen&quot;: true
  },
}
</code></pre>
<h2>迁移数据</h2>
<p>执行迁移命令, 测试 100w 左右数据花了几十秒, 不算很慢, 如果卡住可以切断重试</p>
<pre><code class="language-bash">npm start
</code></pre>
<br/><p>访问 <a href="https://krau.top/posts/mysql-migrate-to-postgresql" target="_blank">https://krau.top/posts/mysql-migrate-to-postgresql</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-05-28T21:49:26.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[Nginx 迷思之请求缓冲与 Go 的分块传输编码]]></title>
        <id>https://krau.top/posts/nginx-req-buffer-and-go-chunk-transfer-encoding/index</id>
        <link href="https://krau.top/posts/nginx-req-buffer-and-go-chunk-transfer-encoding/index"/>
        <updated>2025-03-26T11:06:43.000Z</updated>
        <content type="html"><![CDATA[<p>最近在自己的某个项目中频繁被用户反馈的一个文件上传bug, 测试也总是无法复现, debug 许久才找到问题根源, 以此记录一下.</p>
<!-- more -->
<p>项目中实现了兼容多种服务的文件上传接口的功能, 其中之一就是广为人知的 <a href="https://github.com/AlistGo/alist">Alist</a> .</p>
<p>这部分代码如下:</p>
<pre><code class="language-go">type Alist struct {
	client    *http.Client
	token     string
	baseURL   string
    // ...
}

func (a *Alist) Save(ctx context.Context, reader io.Reader, path string) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+&quot;/api/fs/put&quot;, reader)
	if err != nil {
		return fmt.Errorf(&quot;failed to create request: %w&quot;, err)
	}
	req.Header.Set(&quot;Authorization&quot;, a.token)
	req.Header.Set(&quot;File-Path&quot;, path)
	req.Header.Set(&quot;Content-Type&quot;, &quot;application/octet-stream&quot;)

	resp, err := a.client.Do(req)
	if err != nil {
		return fmt.Errorf(&quot;failed to send request: %w&quot;, err)
	}
	defer resp.Body.Close()

    // ...
}
</code></pre>
<p>自从发布以来, 就收到一些用户反馈无法向 Alist 上传的问题, 但是无论是开发时的测试还是自己的&quot;生产&quot;环境都无法复现.</p>
<p>现在细说来这个问题出现的原因很简单, 根据 <a href="https://alist.nn.ci/zh/guide/api/fs.html#%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0-14">Alist 文档</a> 和 <a href="https://github.com/AlistGo/alist/issues/5319">issue #5319</a> 可知 Alist 的文件上传接口不支持 chunked 模式, 必须传递 <code>Content-Length</code> 指定文件大小.</p>
<p>上面的代码里没有设置 <code>Content-Length</code> , 且请求体是以 <code>io.Reader</code> 传递给 <code>http.Client</code> , 查看 <a href="https://github.com/golang/go/blob/6be4a5eb4898c7b5e7557dda061cc09ba310698b/src/net/http/transfer.go#L82-L96">net/http 源码</a> 就可以发现, 此时就会使用 chunked 模式传输 (<s>其实这本来就是我想要的, 但谁知道 alist 不支持</s>).</p>
<p>而 Alist 应该返回 400 错误: <code>strconv.ParseInt: parsing &quot;&quot;: invalid syntax</code> , 也就是解析不到 <code>Content-Length</code></p>
<p>但是自己测试时却仍然可以上传成功, 是由于我的环境中使用 nginx 反向代理了 Alist , 根据 <a href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering">nginx 文档</a> 可知, nginx 默认开启了请求缓冲 ( <code>proxy_request_buffering</code> ) .</p>
<p>向 nginx 上传文件时, nginx 会将请求体完整地缓冲到内存或硬盘中, 然后将设置了正确 <code>Content-Length</code> 头的新请求转发给后端.</p>
<br/><p>访问 <a href="https://krau.top/posts/nginx-req-buffer-and-go-chunk-transfer-encoding/index" target="_blank">https://krau.top/posts/nginx-req-buffer-and-go-chunk-transfer-encoding/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2025-03-26T11:06:43.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[Linux 安装最新 FFmpeg 和 FFprobe]]></title>
        <id>https://krau.top/posts/linux-install-ffmpeg/index</id>
        <link href="https://krau.top/posts/linux-install-ffmpeg/index"/>
        <updated>2024-12-01T15:39:48.000Z</updated>
        <content type="html"><![CDATA[<p>大多数 Linux 发行版的软件源中的 FFmpeg 版本都比较老旧，本文介绍如何在 Linux 系统上安装新版或任意版本的 FFmpeg 和 FFprobe.</p>
<!-- more -->
<p>::: tip<br>
示例环境: Debian 12<br>
:::</p>
<p><s>水文章</s></p>
<h2>1. 下载静态编译包</h2>
<pre><code class="language-shell">wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
</code></pre>
<p>这里下载的是最新适用于 amd64 架构的 ffmpeg + ffprobe 静态编译包，如果你的系统是其他架构，可以到 <a href="https://johnvansickle.com/ffmpeg/">johnvansickle.com</a> 下载对应的包。</p>
<p>旧版可以在 <a href="https://www.johnvansickle.com/ffmpeg/old-releases/">old releases</a> 下载.</p>
<h2>2. 解压</h2>
<pre><code class="language-shell">tar -xvf ffmpeg-release-amd64-static.tar.xz
</code></pre>
<h2>3. 添加到 PATH</h2>
<pre><code class="language-shell">cd ffmpeg-*-static
sudo mv ffmpeg /usr/local/bin/
sudo mv ffprobe /usr/local/bin/
</code></pre>
<h2>4. 验证</h2>
<pre><code class="language-shell">ffmpeg -version
</code></pre>
<p><img src="https://krau.top/assets/2024-12-01-15-48-44.BHTRrW4n.webp" alt="0"></p>
<br/><p>访问 <a href="https://krau.top/posts/linux-install-ffmpeg/index" target="_blank">https://krau.top/posts/linux-install-ffmpeg/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2024-12-01T15:39:48.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[部署跨多数据中心分布的 MongoDB 副本集]]></title>
        <id>https://krau.top/posts/deploy-mongodb-replica-set/index</id>
        <link href="https://krau.top/posts/deploy-mongodb-replica-set/index"/>
        <updated>2024-08-18T19:05:10.000Z</updated>
        <content type="html"><![CDATA[<p><img src="https://krau.top/photo/posts/deploy-mongodb-replica-set/mongodbreplicaset.webp" alt="MongoDB"></p>
<p>最近把自己的一个小项目迁移了服务器, 结果发现接口响应速度慢了很多, 排查了不少时间后突然想起来是因为新服务器和 MongoDB Atlas 不在同一个地区, 光是二者之间的网络延迟就很高.</p>
<p>而 MongoDB Atlas 提供的免费实例要备份数据和迁移比较麻烦, 又因为 MongoDB 单实例不支持事务, 所以决定干脆自建一个 MongoDB 副本集 😎.</p>
<!-- more -->
<h2>环境</h2>
<p>::: tip 本文环境</p>
<ul>
<li>OS: Debian Bookworm</li>
<li>MongoDB: 7.0.0</li>
<li>使用 Docker 安装 MongoDB<br>
:::</li>
</ul>
<p>我手上有几台不同服务商不同地区的服务器, 下文都以 <code>Server A</code>, <code>Server B</code>, <code>Server C</code> 代表.</p>
<p>为了申请证书, 还需要至少一个域名.</p>
<h2>安装 MongoDB</h2>
<p>首先要在每台服务器上都安装 MongoDB, 这里使用 Docker Compose.</p>
<pre><code class="language-yaml"># docker-compose.yml
services:
  mongo:
    image: mongo
  #  restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - 27017:27017
    volumes:
      - ./data:/data/db
      - ./config:/etc/mongo
    command: [&quot;--config&quot;, &quot;/etc/mongo/mongod.conf&quot;]
</code></pre>
<p>然后 <code>./config</code> 目录下新建我们的配置文件 <code>mongod.conf</code>:</p>
<pre><code class="language-yaml"># mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db
#  engine:
#  wiredTiger:

# network interfaces
net:
  port: 27017
  bindIp: 127.0.0.1

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

# where to write logging data.
# systemLog:
#   destination: file
#   logAppend: true
#   path: /var/log/mongodb/mongod.log
</code></pre>
<p>暂时只需要上面这一点配置就可以啦, 此时可以先启动容器, 接下来需要配置副本集之间认证用的 keyfile.</p>
<h2>配置节点 keyfile</h2>
<h3>生成 keyfile</h3>
<p><a href="https://www.mongodb.com/zh-cn/docs/manual/core/security-internal-authentication/#std-label-internal-auth-keyfile">keyFile</a> 用于 MongoDB 副本集节点之间的认证, 每一个节点需要有相同的 keyFile 才能加入副本集.</p>
<p>keyFile 的内容就相当于密码啦, 用什么软件生成一个都行, 这里就使用 <code>openssl</code>:</p>
<pre><code class="language-bash">openssl rand -base64 756 &gt; keyfile
</code></pre>
<p>然后需要把 <code>keyfile</code> 分发到每一个节点上, 并且仅为文件所有者提供读取权限.</p>
<h3>修改 keyfile 权限</h3>
<p>如果你是在本机直接安装的 MongoDB, 只需要使用 <code>chmod</code> 修改权限:</p>
<pre><code class="language-bash">chmod 400 keyfile
</code></pre>
<p>但是如果使用 Docker 安装, 不仅要把 <code>keyfile</code> 挂载到容器内, 还要进入容器来修改权限, 因为容器内部的文件权限和外面是不一样的!</p>
<p>这里我把 keyfile 挂载到了 <code>/etc/mongo/keyfile</code></p>
<p>然后进入容器修改权限:</p>
<pre><code class="language-bash">docker exec -it &lt;container_id&gt; /bin/bash
chmod 400 /etc/mongo/keyfile
chown 999:999 /etc/mongo/keyfile
</code></pre>
<p>修改完权限后把容器停掉, 因为我们还要修改配置文件.</p>
<p>::: tip<br>
每一个节点都要使用相同的 keyfile 进行上面相同的操作.<br>
:::</p>
<h2>配置副本集认证</h2>
<p>配置完了所有节点的 keyfile, 现在来修改配置文件启用副本集, 依然是所有节点的配置文件都要修改.</p>
<pre><code class="language-yaml"># mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db
#  engine:
#  wiredTiger:

# network interfaces
net:
  port: 27017
  bindIp: 127.0.0.1

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

# where to write logging data.
# systemLog:
#   destination: file
#   logAppend: true
#   path: /var/log/mongodb/mongod.log

security: // [!code ++]
  clusterAuthMode: keyFile // [!code ++]
  authorization: enabled // [!code ++]
  keyFile: /etc/mongo/keyfile // [!code ++]

replication: // [!code ++]
  replSetName: &quot;rs0&quot; # 副本集名称 // [!code ++]
</code></pre>
<p>如果是同机房所有节点使用内部网络的话, 这样就可以直接启动副本集了, 但是我的节点是分布在不同地区的, 所以还需要开放外部连接.</p>
<pre><code class="language-yaml"># mongod.conf
net:
  port: 27017
  bindIp: 0.0.0.0 // [!code warning]
</code></pre>
<p>既然开放了外部连接, 那么就需要对传输使用 TLS 加密.</p>
<h2>配置 TLS</h2>
<h3>准备证书</h3>
<p>使用 TLS 就需要证书, 这里使用 <code>Let's Encrypt</code> 的证书.</p>
<p>申请证书的方式和为网站申请证书一样, 这里不再赘述, 可以参考:</p>
<ul>
<li><a href="https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E">Acme.sh Wiki</a></li>
<li><a href="/posts/acmessl/index.md">使用ACME申请并配置SSL证书</a></li>
</ul>
<p>::: tip<br>
也可以使用自签名证书, 但其实自己维护一个 CA 比申请第三方 CA 的证书麻烦多了, 可以参考:</p>
<ul>
<li><a href="/posts/grpc-and-protobuf-in-go-tls/index.md">Go 的 gRPC 和 Protocol Buffers---TLS 认证</a></li>
</ul>
<p>而且自签名证书也不够安全.<br>
:::</p>
<p>一般来说, 申请成功证书会得到两个文件: <code>fullchain.pem</code> 和 <code>privkey.pem</code>, 分别是证书链和私钥.</p>
<p>但是 MongoDB 还需要 CA 证书, 可以在 <a href="https://letsencrypt.org/certs/isrgrootx1.pem">Let's Encrypt ISRG Root X1</a> 下载.</p>
<p>接下来需要把 <code>fullchain.pem</code>, <code>privkey.pem</code> '合并' 为一个文件 (MongoDB 要求的!)</p>
<pre><code class="language-bash">cat fullchain.pem privkey.pem &gt; mongod.pem
</code></pre>
<p>其实就是把 <code>fullchain.pem</code> 和 <code>privkey.pem</code> 的内容拼接到一起, 得到的 <code>mongod.pem</code> 就是 MongoDB 需要的证书文件.</p>
<p>::: tip 可以使用泛域名证书<br>
如果你打算把所有服务器解析到一个域名上, 那么可以使用泛域名证书, 这样就不用为每一个节点都单独申请证书了.<br>
:::</p>
<h3>配置证书</h3>
<p>准备好每个节点自己的 <code>mongod.pem</code>, 把 CA 根证书 <code>isrgrootx1.pem</code> 拷贝给每一个节点. 我这里仍是直接挂载在了容器的 <code>/etc/mongo/</code> 目录.</p>
<p>然后修改它们的配置文件:</p>
<pre><code class="language-yaml">net:
  port: 27017
  bindIp: 0.0.0.0

  tls:
    mode: requireTLS // [!code ++]
    certificateKeyFile: /etc/mongo/mongod.pem // [!code ++]
    CAFile: /etc/mongo/isrgrootx1.pem // [!code ++]
    allowConnectionsWithoutCertificates: true // [!code ++]
</code></pre>
<p>关于 <code>net.tls</code> 的配置详情参考:</p>
<ul>
<li><a href="https://www.mongodb.com/zh-cn/docs/manual/reference/configuration-options/#net.tls-options">MongoDB TLS/SSL Configuration</a></li>
</ul>
<p>配置完证书后可以准备启动副本集了.</p>
<h2>完整 mongod.conf</h2>
<pre><code class="language-yaml"># mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db

net:
  port: 27017
  bindIp: 0.0.0.0

  tls:
    mode: requireTLS
    certificateKeyFile: /etc/mongo/mongodb.pem
    CAFile: /etc/mongo/isrgrootx1.pem
    allowConnectionsWithoutCertificates: true

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

security:
  clusterAuthMode: keyFile
  authorization: enabled
  keyFile: /etc/mongo/keyfile

replication:
  replSetName: &quot;rs0&quot;
</code></pre>
<h2>启动副本集</h2>
<table>
<thead>
<tr>
<th>示例节点名</th>
<th>域名</th>
</tr>
</thead>
<tbody>
<tr>
<td>Server A</td>
<td><a href="http://servera.mongodb.example.com">servera.mongodb.example.com</a></td>
</tr>
<tr>
<td>Server B</td>
<td><a href="http://serverb.mongodb.example.com">serverb.mongodb.example.com</a></td>
</tr>
<tr>
<td>Server C</td>
<td><a href="http://serverc.mongodb.example.com">serverc.mongodb.example.com</a></td>
</tr>
</tbody>
</table>
<p>启动所有节点, 然后使用支持的客户端连接你打算作为主节点的节点, 使用 <code>rs.initiate()</code> 初始化副本集, 以 <code>Server A</code> 为例:</p>
<p>::: warning<br>
只在一个节点上执行 <code>rs.initiate()</code>!<br>
:::</p>
<pre><code class="language-js">rs.initiate( {
   _id : &quot;rs0&quot;,
   members: [
      { _id: 0, host: &quot;servera.mongodb.example.com:27017&quot; },
      { _id: 1, host: &quot;serverb.mongodb.example.com:27017&quot; },
      { _id: 2, host: &quot;serverc.mongodb.example.com:27017&quot; }
   ]
})
</code></pre>
<p>或者, 也可以不向 <code>rs.initiate()</code> 传入配置, 而是初始化后再添加节点:</p>
<pre><code class="language-js">rs.initiate()
rs.add(&quot;serverb.mongodb.example.com:27017&quot;)
rs.add(&quot;serverc.mongodb.example.com:27017&quot;)
</code></pre>
<p>参考:</p>
<ul>
<li><a href="https://www.mongodb.com/zh-cn/docs/manual/tutorial/deploy-replica-set/#deploy-a-replica-set-in-the-terminal">在终端中部署副本集</a></li>
<li><a href="https://www.mongodb.com/zh-cn/docs/manual/tutorial/expand-replica-set/#procedures">将成员添加到自管理副本集</a></li>
</ul>
<p>如果你的操作都正确, 那这里应该已经启动成功了 😇. 使用 <code>rs.status()</code> 查看副本集状态:</p>
<p><img src="https://krau.top/assets/2024-08-19-15-08-49.xqqg267Q.webp" alt="0"></p>
<p><code>PRIMARY</code> 就是主节点, <code>SECONDARY</code> 是从节点.</p>
<p>如果你发现有其他状态的节点, 可以查看 <a href="https://www.mongodb.com/zh-cn/docs/manual/reference/replica-states/">副本集成员状态</a> 排查问题.</p>
<h2>连接副本集</h2>
<p>在上文, 我们连接到了主节点来初始化副本集, 它的连接字符串通常是像这样的:</p>
<pre><code class="language-text">mongodb://user:password@servera.mongodb.example.com:27017
</code></pre>
<p>但这样并不是连接到了 &quot;整个&quot; 副本集. 显然如果主节点挂了, 连接就会断开.</p>
<p>其实 MongoDB 可以使用多服务器连接字符串, 例如:</p>
<pre><code class="language-text">mongodb://user:pssword@servera.mongodb.example.com:27017,serverb.mongodb.example.com:27017,serverc.mongodb.example.com:27017/?replicaSet=rs0
</code></pre>
<p>服务器之间用逗号分隔, 这样就可以连接到整个副本集了, 如果主节点挂了, 客户端会自动切换到其他节点.</p>
<p>但是这样的连接字符串太长了, 十分不方便, 这时可以使用 SRV 记录来连接.</p>
<h3>SRV 记录</h3>
<p>SRV 记录（Service Record）是一种 DNS 记录, 用于指定特定服务在域中的主机名和端口号. 可以用于负载均衡和服务发现, 使客户端可以动态地查找提供特定服务的服务器.</p>
<p>SRV 记录的设置需要在 DNS 服务商处设置, 以 Cloudflare 为例:</p>
<p><img src="https://krau.top/assets/2024-08-19-15-30-23.DAIIQCzd.webp" alt="1"></p>
<table>
<thead>
<tr>
<th>选项</th>
<th>值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>_mongodb._tcp.mongodbsrv</td>
<td>SRV 记录的 Name 由服务名和域名组成, <code>_mongodb._tcp</code> 为服务名, 这里 <code>mongodbsrv</code> 是二级域名</td>
</tr>
<tr>
<td>Priority</td>
<td>0</td>
<td>优先级, 对于 MongoDB 副本集不需要特别设置</td>
</tr>
<tr>
<td>Weight</td>
<td>1</td>
<td>权重, 对于 MongoDB 副本集不需要特别设置</td>
</tr>
<tr>
<td>Port</td>
<td>27017</td>
<td>端口号</td>
</tr>
<tr>
<td>Target</td>
<td><a href="http://servera.mongodb.example.com">servera.mongodb.example.com</a></td>
<td>主机名, 填写解析到节点的域名</td>
</tr>
</tbody>
</table>
<p>要为每一个节点都设置 Name 相同的 SRV 记录哦.</p>
<p>::: tip<br>
设置 SRV 记录的域名和解析到节点使用的域名不一定要相同<br>
:::</p>
<p>假设我们设置 SRV 记录的域名是 <code>example.com</code>, 接下来就可以使用 <code>mongodb+srv</code> 的字符串格式来连接副本集了:</p>
<pre><code class="language-text">mongodb+srv://user:password@mongodbsrv.example.com/?replicaSet=rs0
</code></pre>
<p>🎉</p>
<h2>FAQ</h2>
<p>一些其他的自问自答:</p>
<ol>
<li>
<p>为什么集群内认证不使用 X.509 证书?</p>
<p>太麻烦了, 使用 X.509 在节点间认证实际上是使用了基于角色的访问控制, 要在 <code>$external</code> 数据库中为节点配置角色, 还要自己维护 CA.</p>
</li>
<li>
<p>配置中的 <code>tls.mode</code>: <code>requireTLS</code> 和 <code>allowConnectionsWithoutCertificates</code>: <code>true</code> 是不是冲突的?</p>
<p>并不, <code>requireTLS</code> 是要求客户端使用 TLS 连接, <code>allowConnectionsWithoutCertificates</code> 是允许客户端不使用证书进行身份验证.</p>
</li>
<li>
<p>为什么不配置 <code>net.tls.clusterFile</code> ?</p>
<p>因为我们对集群内使用的是密钥文件 (keyfile) 验证.</p>
<p><code>clusterFile</code> 用于集群内使用 X.509 证书进行身份验证.</p>
<p>即使集群内使用 X.509 证书验证, 不配置 <code>clusterFile</code>, MongoDB 也会自动使用 <code>certificateKeyFile</code> 的证书进行验证.</p>
</li>
</ol>
<br/><p>访问 <a href="https://krau.top/posts/deploy-mongodb-replica-set/index" target="_blank">https://krau.top/posts/deploy-mongodb-replica-set/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2024-08-18T19:05:10.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[在 WSL2 上安装 CUDA]]></title>
        <id>https://krau.top/posts/install-cuda-on-wsl/index</id>
        <link href="https://krau.top/posts/install-cuda-on-wsl/index"/>
        <updated>2024-07-01T19:32:40.000Z</updated>
        <content type="html"><![CDATA[<p>最近在写一些小东西的时候需要用到显卡, 但是 Windows 的开发体验又很差 (尤其是对于 Python), 所以就在 WSL 上安装了 CUDA 以供使用.</p>
<!-- more -->
<p>::: tip<br>
本文环境:</p>
<ul>
<li>OS: WSL2 Debian bookworm</li>
<li>GPU: RTX 3060 Laptop<br>
:::</li>
</ul>
<h2>更新驱动</h2>
<p>CUDA 所需要的驱动版本一般挺新的, 所以需要<strong>在 Windows 上</strong>更新显卡驱动. 使用 Geforce Experience 或者去 <a href="https://www.nvidia.com/download/index.aspx">官网下载页面</a> 寻找符合你型号的驱动版本即可.</p>
<p>::: warning<br>
注意是直接在 Windows 上更新, 不要跑去 WSL 里安装英伟达<s>那恶心</s>的 Linux 驱动了.<br>
:::</p>
<p>更新完成后重启 WSL, 使用 <code>nvidia-smi</code> 查看信息:</p>
<p><img src="https://krau.top/assets/2024-07-01-19-55-46.qzQT9iOl.webp" alt="0"></p>
<h2>安装 CUDA Toolkit</h2>
<p>在 <a href="https://developer.nvidia.com/cuda-downloads">英伟达下载页面</a> 选择 WSL-Ubuntu 发行版, 选择 <strong>runfile (local)</strong> 进行下载.</p>
<pre><code class="language-bash">wget https://developer.download.nvidia.com/compute/cuda/12.4.1/local_installers/cuda_12.4.1_550.54.15_linux.run
</code></pre>
<p>::: tip<br>
留意你所需要的版本, 历史版本可去 <a href="https://developer.nvidia.com/cuda-toolkit-archive">归档页</a> 下载, 本文下载的是 12.4 版本.<br>
:::</p>
<p>下载完成后运行安装:</p>
<pre><code class="language-bash">sudo sh cuda_12.4.1_550.54.15_linux.run
</code></pre>
<p>安装过程中会询问 EULA, 输入 <code>accept</code> 即可.</p>
<p>::: warning<br>
不要更改默认安装选项, 同意 EULA 后应直接开始安装. 否则大概会卡住..<br>
:::</p>
<p>安装完成后, 更新 <code>PATH</code> 和 <code>LD_LIBRARY_PATH</code>, 将下述内容添加到你的配置文件里 (如 <code>~/.bashrc</code> 或 <code>~/.zshrc</code>):</p>
<pre><code class="language-bash">export PATH=&quot;/usr/local/cuda-12.4/bin:$PATH&quot;
export LD_LIBRARY_PATH=&quot;/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH&quot;
</code></pre>
<p>重启终端使配置生效, 使用 <code>nvcc --version</code> 查看版本信息:</p>
<p><img src="https://krau.top/assets/2024-07-01-20-08-26.cFOX8bdW.webp" alt="1"></p>
<h2>安装 cuDNN</h2>
<p>在 <a href="https://developer.nvidia.com/cudnn-downloads">英伟达下载页面</a> 下载 cuDNN, 选择与你环境相符的版本下载. 本文下载 cuDNN 9.2.0 for CUDA 12.4:</p>
<pre><code class="language-bash">wget https://developer.download.nvidia.com/compute/cudnn/9.2.0/local_installers/cudnn-local-repo-debian12-9.2.0_1.0-1_amd64.deb
sudo apt install ./cudnn-local-repo-debian12-9.2.0_1.0-1_amd64.deb
</code></pre>
<p>添加 keyring 和源:</p>
<pre><code class="language-bash">sudo cp /var/cuda-repo-debian12-9-2-local/cudnn-*-keyring.gpg /usr/share/keyrings/
sudo sed -i &quot;s|^Components: main$|&amp; contrib|&quot; /etc/apt/sources.list.d/cudnn-local-debian12-9.2.0.list
</code></pre>
<p><em>注意根据 apt 的输出修改上述两行命令.</em></p>
<p>安装 cuDNN:</p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y cudnn-cuda-12
</code></pre>
<br/><p>访问 <a href="https://krau.top/posts/install-cuda-on-wsl/index" target="_blank">https://krau.top/posts/install-cuda-on-wsl/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2024-07-01T19:32:40.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[Go 的 gRPC 和 Protocol Buffers---TLS 认证]]></title>
        <id>https://krau.top/posts/grpc-and-protobuf-in-go-tls</id>
        <link href="https://krau.top/posts/grpc-and-protobuf-in-go-tls"/>
        <updated>2023-10-23T09:29:29.000Z</updated>
        <content type="html"><![CDATA[<h2>前言</h2>
<p>为了保证 gRPC 服务不被第三方监听和调用, 防止通信被篡改或伪造, 需要对 gRPC 服务添加身份验证机制.</p>
<p>目前 gRPC 内置了三种身份验证机制:</p>
<ul>
<li><strong>SSL/TLS</strong>: gRPC 集成了 SSL/TLS, 提倡使用 SSL/TLS 对服务器进行身份验证, 并对客户端与服务器之间交换的所有数据进行加密。同时可选让客户端提供用于相互验证的证书.</li>
<li><strong>基于 Token 的身份验证</strong>: gRPC 提供了一种可将基于元数据的凭证附加到请求和响应中的机制. 这种机制必须与 SSL/TLS 同时使用, 以提供完整的身份验证和加密通信.</li>
<li><strong>ATLS</strong>: Google 自家的双向身份验证和传输安全系统, 通常也用于在 Google 家的平台上构建服务时.</li>
</ul>
<p>本文介绍 gRPC 中的 SSL/TLS.</p>
<!-- more -->
<p>::: tip<br>
本文环境</p>
<ul>
<li>Debian 12 amd64</li>
<li>Go 1.21.3<br>
:::</li>
</ul>
<h2>SAN, CA 和证书</h2>
<p>使用 TLS 就需要证书. 在 Go 1.15 版本之后废弃了 <code>CommonName</code>, 而是要使用 <code>Subject Alternative Name</code> (SAN) 来验证证书的有效性. 因此如果按照以前的方法生成证书, 大概率会导致下面的错误:</p>
<blockquote>
<p><code>rpc error: code = Unavailable desc = connection error: desc = &quot;transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0&quot;</code></p>
</blockquote>
<p>SAN 是 x509 定义中的一个扩展, 用于指定证书的使用范围. 使用了 SAN 扩展的证书, 可以在证书中指定多个域名或 IP 地址, 从而可以在一个证书中同时包含多个域名或 IP 地址.</p>
<p>CA (Certificate Authority), 即证书颁发机构, 用于管理和签发证书. 作用是检查证书持有者的合法性, 并颁发证书, 防止证书被伪造.</p>
<p>CA 自己也需要有证书, 称为根证书, 用于签发其他证书. 一般情况下, 根证书是自签名的, 即自己给自己签发证书. 根证书是信任链的起点, 有了根证书之后 CA 才能给其他人签发证书.</p>
<p>因此, 要想在 gRPC 中使用 TLS, 需要准备三组证书:</p>
<ul>
<li>CA 根证书</li>
<li>服务端证书</li>
<li>客户端证书</li>
</ul>
<h2>生成证书</h2>
<h3>生成 CA 证书</h3>
<p>在任意目录下创建 <code>ca.conf</code> 文件:</p>
<pre><code class="language-ini">[ req ]
default_bits       = 4096
distinguished_name = moe

[ moe ]
countryName                 = GB
countryName_default         = BeiJing
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = BeiJing
localityName                = Locality Name (eg, city)
localityName_default        = NanJing
organizationName            = Organization Name (eg, company)
organizationName_default    = Kompany
commonName                  = krau.top
commonName_max              = 64
commonName_default          = krau.top
</code></pre>
<p>执行下面的命令生成 CA 私钥:</p>
<pre><code class="language-bash">openssl genrsa -out ca.key 4096
</code></pre>
<p>然后生成 CA 证书:</p>
<pre><code class="language-bash">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config ca.conf
</code></pre>
<p>根据提示输入各种信息, 最后会生成 <code>ca.crt</code> 文件.</p>
<h3>生成服务端证书</h3>
<p>新建 <code>server.conf</code>:</p>
<pre><code class="language-ini">[ req ]
default_bits       = 2048
distinguished_name = moe

[ moe ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = JiangSu
localityName                = Locality Name (eg, city)
localityName_default        = NanJing
organizationName            = Organization Name (eg, company)
organizationName_default    = ovocom
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = localhost # 此值应该包含在 [alt_names] 中
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1   = localhost
IP      = 127.0.0.1
</code></pre>
<p>::: warning<br>
<code>commonName_default</code> 的值应该包含在 <code>[alt_names]</code> 中, 并且在连接时, 客户端的值需要与其匹配.<br>
:::</p>
<p>生成私钥:</p>
<pre><code class="language-bash">openssl genrsa -out server.key 2048
</code></pre>
<p>生成 csr:</p>
<pre><code class="language-bash">openssl req -new -key server.key -out server.csr -config server.conf
</code></pre>
<p>请求 CA 签发证书:</p>
<pre><code class="language-bash">openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extensions req_ext -extfile server.conf
</code></pre>
<h3>生成客户端证书</h3>
<p>客户端不需要配置文件, 直接生成私钥:</p>
<pre><code class="language-bash">openssl genrsa -out client.key 2048
</code></pre>
<p>生成 csr:</p>
<pre><code class="language-bash">openssl req -new -key client.key -out client.csr
</code></pre>
<p>请求 CA 签发证书:</p>
<pre><code class="language-bash">openssl x509 -req -days 3650 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
</code></pre>
<p>现在已经准备好证书了. 服务端需要 <code>server.crt</code> 和 <code>server.key</code>, 客户端需要 <code>client.crt</code> 和 <code>client.key</code>. 此外, 两者都需要 <code>ca.crt</code>.</p>
<h2>实现</h2>
<h3>服务端</h3>
<p>关注高亮行</p>
<pre><code class="language-go">package main

import (
	&quot;crypto/tls&quot;
	&quot;crypto/x509&quot;
	&quot;net&quot;
	&quot;os&quot;
    &quot;fmt&quot;

	&quot;google.golang.org/grpc&quot;
	&quot;google.golang.org/grpc/credentials&quot;
    &quot;example/proto&quot;
)

func main() {

	pair, err := tls.LoadX509KeyPair(&quot;./server.crt&quot;, &quot;./server.key&quot;)
	if err != nil {
		fmt.Printf(&quot;Failed to load certificates: %s&quot;, err)
		return
	}

	certPool := x509.NewCertPool()
	ca, err := os.ReadFile(&quot;./ca.crt&quot;) // 读取根证书
	if err != nil {
		fmt.Printf(&quot;Failed to read ca certificate: %s&quot;, err)
		return
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		fmt.Println(&quot;Failed to append ca certificate&quot;)
		return
	}
	creds := credentials.NewTLS(&amp;tls.Config{
		Certificates: []tls.Certificate{pair},
		ClientAuth:   tls.RequireAndVerifyClientCert,
		ClientCAs:    certPool,
	})
	s := grpc.NewServer(grpc.Creds(creds))

	proto.RegisterArtworkServiceServer(s, &amp;server{})
	lis, err := net.Listen(&quot;tcp&quot;, &quot;:39010&quot;)
    if err != nil {
        fmt.Printf(&quot;Failed to listen: %s&quot;, err)
        return
    }
	if err := s.Serve(lis); err != nil {
		fmt.Printf(&quot;Failed to serve: %s&quot;, err)
		return
	}
}
</code></pre>
<h3>客户端</h3>
<p>关注高亮行</p>
<pre><code class="language-go">package main

import (
	&quot;crypto/tls&quot;
	&quot;crypto/x509&quot;
	&quot;os&quot;

	&quot;example/proto&quot;
	&quot;google.golang.org/grpc&quot;
	&quot;google.golang.org/grpc/credentials&quot;
)


func main() {
	pair, err := tls.LoadX509KeyPair(&quot;./client.crt&quot;, &quot;./client.key&quot;)
	if err != nil {
		fmt.Printf(&quot;Failed to load certificates: %s&quot;, err)
		return
	}
	certPool := x509.NewCertPool()
	ca, err := os.ReadFile(&quot;./ca.crt&quot;)
	if err != nil {
        fmt.Printf(&quot;Failed to read ca certificate: %s&quot;, err)
		return
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		fmt.Println(&quot;Failed to append ca certificate&quot;)  
		return
	}
	cred := credentials.NewTLS(&amp;tls.Config{
		Certificates: []tls.Certificate{pair},
		ServerName:   &quot;localhost&quot;, // 服务端生成证书时的 commonName
		RootCAs:      certPool,
	})
	conn, err := grpc.Dial(&quot;127.0.0.1:39010&quot;, grpc.WithTransportCredentials(cred))
	if err != nil {
		fmt.Printf(&quot;Failed to dial: %s&quot;, err)
		return
	}
	defer conn.Close()
    client := proto.NewArtworkServiceClient(conn)
    // ...
}
</code></pre>
<h2>参考</h2>
<ul>
<li><a href="https://grpc.io/docs/guides/auth/">gRPC Authentication</a></li>
</ul>
<br/><p>访问 <a href="https://krau.top/posts/grpc-and-protobuf-in-go-tls" target="_blank">https://krau.top/posts/grpc-and-protobuf-in-go-tls</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-10-23T09:29:29.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[Go 的 gRPC 和 Protocol Buffers---Quick Start]]></title>
        <id>https://krau.top/posts/grpc-and-protobuf-in-go-start</id>
        <link href="https://krau.top/posts/grpc-and-protobuf-in-go-start"/>
        <updated>2023-10-21T21:27:48.000Z</updated>
        <content type="html"><![CDATA[<h2>Protocol Buffers</h2>
<p>Protocol Buffers (Protobuf) 是一种语言无关、平台无关、可扩展的序列化结构化数据的方法, 可以像 XML, json 等一样用于程序间数据通信. 而相较于上列二者, Protobuf 有更高的效率.</p>
<p>使用 Protobuf 需要在 <code>.proto</code> 文件中定义数据结构, 并安装其编译器来生成对应语言的模型. 本文以 Go 为例.</p>
<!-- more -->
<h3>安装</h3>
<p>::: tip<br>
本文环境:</p>
<ul>
<li>Debian 12 amd64</li>
<li>Go 1.21.3</li>
<li>Protoc 24.4<br>
:::</li>
</ul>
<p>安装 Protobuf 编译器最简单的方法是在 <a href="https://github.com/protocolbuffers/protobuf/releases">Github Release</a> 页面下载对应平台的二进制文件, 并加入 <code>PATH</code> 环境变量.</p>
<pre><code class="language-bash">wget https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip
mkdir protoc &amp;&amp; unzip protoc-24.4-linux-x86_64.zip -d protoc
export PATH=$PATH:/path/to/protoc/bin # 将此行加入终端配置文件中
</code></pre>
<p>为了在 Go 中使用 Protobuf 和默认使用 Protobuf 的 gRPC, 还需要安装 <code>protoc-gen-go</code> 和 <code>protoc-gen-go-grpc</code></p>
<pre><code class="language-bash">go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH=&quot;$PATH:$(go env GOPATH)/bin&quot; # 把 GOBIN 加入 PATH
</code></pre>
<h3>语法和规范</h3>
<p>Protobuf 的主要语法很简单也很 Go, 下例:</p>
<pre><code class="language-proto">syntax = &quot;proto3&quot;; // 指定版本

option go_package = &quot;example/proto&quot;; // 指定 Go 代码生成的包名

package artwork; // 包名, 用于区分不同的 proto 文件. 注意此包名并不是 Go 中的包名

// 定义消息类型
message ArtworkInfo {
    uint64 artworkID = 1; 
    string title = 2;
    SourceName source = 3; // 引用枚举类型
    repeated string tags = 4; // repeated 表示为数组
    bool r18 = 5;
    repeated PictureInfo pictures = 6; // 引用嵌套消息类型

    // 定义枚举类型
    enum SourceName {
        Pixiv = 0;
        Twitter = 1;
        // ...
    }

    // 定义嵌套消息类型
    message PictureInfo {
        uint64 pictureID = 1;
        uint64 width = 2;
        uint64 height = 3;
    }
}
</code></pre>
<p>可以看到 Proto 的消息定义和 Go 的结构体定义很像, 简要总结:</p>
<ul>
<li>消息类型定义: <code>message</code> 关键字 + 消息名, 在大括号内定义字段</li>
<li>字段定义: 字段类型, 字段名, 编号. 编号是不可省略且在同一消息定义下不可重复的. 可以理解成字段的唯一ID.</li>
</ul>
<p>在规范上, Protobuf 有如下约定:</p>
<ul>
<li>包名应为小写, 且使用和目录结构一致的点分法, 如 <code>example.package</code></li>
<li>使用驼峰命名法命名消息类型和服务, 如 <code>ExampleMessage</code>, <code>ExampleService</code></li>
<li>使用蛇形命名法命名字段, 如 <code>example_field</code></li>
<li>使用双斜杠注释, 如 <code>// 注释内容</code></li>
</ul>
<p>关于 Protobuf 的数据类型和各语言中的对应, 可以参考 <a href="https://developers.google.com/protocol-buffers/docs/proto3#scalar">官方文档#scalar</a></p>
<h3>编译生成 Go 代码</h3>
<p>在 <code>.proto</code> 文件所在目录下执行:</p>
<pre><code class="language-bash">protoc --go_out=. example.proto
</code></pre>
<p>protoc 会在当前目录下生成 <code>proto/example.pb.go</code> 文件, 其中包含了 <code>example.proto</code> 中定义的消息类型, 并且提供了一些方便的方法.</p>
<p>由 protoc 生成的文件不应直接修改, 如需更改, 应修改 <code>.proto</code> 文件后重新编译生成.</p>
<p>Protobuf 常与 gRPC 结合使用, 作为服务之间远程调用的数据传输格式.</p>
<h2>gRPC</h2>
<p><a href="https://grpc.io/">gRPC</a> 是一个基于 HTTP/2 的&quot;高性能、开源和通用的 <a href="https://zh.wikipedia.org/wiki/%E8%BF%9C%E7%A8%8B%E8%BF%87%E7%A8%8B%E8%B0%83%E7%94%A8">RPC</a> 框架&quot;. gRPC 默认使用 Protobuf 作为接口定义语言和数据传输格式.</p>
<p>gRPC、 Protobuf 和 Go 都是由 Google 开发的, <s>这三者结合使用具有套装效果.</s></p>
<h3>安装</h3>
<p>在项目目录下执行:</p>
<pre><code class="language-bash">go get -u google.golang.org/grpc
</code></pre>
<h3>定义 gRPC 服务和方法</h3>
<p>定义一个服务, 并指定其可以被远程调用的方法, 以及方法的参数和返回值类型. 这即是 gRPc 定义服务的思想.</p>
<p>在上文的 <code>example.proto</code> 中定义一个服务:</p>
<pre><code class="language-proto">syntax = &quot;proto3&quot;; // 指定版本

option go_package = &quot;example/proto&quot;; // 指定 Go 代码生成的包名

package artwork; // 包名, 用于区分不同的 proto 文件. 注意此包名并不是 Go 中的包名

// 定义消息类型
message ArtworkInfo {
    uint64 artworkID = 1; 
    string title = 2;
    SourceName source = 3; // 引用枚举类型
    repeated string tags = 4; // repeated 表示为数组
    bool r18 = 5;
    repeated PictureInfo pictures = 6; // 引用嵌套消息类型

    // 定义枚举类型
    enum SourceName {
        Pixiv = 0;
        Twitter = 1;
        // ...
    }

    // 定义嵌套消息类型
    message PictureInfo {
        uint64 pictureID = 1;
        uint64 width = 2;
        uint64 height = 3;
    }
}

message GetArtworkInfoRequest { // [!code ++]
    uint64 artworkID = 1; // [!code ++]
} // [!code ++]

// 定义服务 // [!code ++]
service ArtworkService { // [!code ++]
    // 定义方法 // [!code ++]
    rpc GetArtworkInfo (GetArtworkInfoRequest) returns (ArtworkInfo);// [!code ++]
} // [!code ++]
</code></pre>
<h4>gRPC 方法</h4>
<p>gRPC 有四种方法类型:</p>
<ol>
<li>一元方法</li>
<li>服务端流式方法</li>
<li>客户端流式方法</li>
<li>双向流式方法</li>
</ol>
<h5>一元方法</h5>
<p>一元方法是最简单常用的方法. 客户端将请求消息作为参数发送给服务, 服务返回一个响应消息. 一元方法调用完成.</p>
<p>在实际代码中, 一元方法的调用就像是调用一个普通的函数.</p>
<p>上文的 <code>GetArtworkInfo</code> 方法就是一个一元方法.</p>
<h5>服务端流式方法</h5>
<p>服务端流式方法是指客户端发送一个请求消息, 服务端返回一个流式响应消息. 客户端从返回的流中读取响应消息, 直到没有消息为止.</p>
<p>定义流式方法, 需要在参数或返回值前加上 <code>stream</code> 关键字.</p>
<p>例如, 定义一个 <code>GetArtworkInfoList</code> 方法, 用于获取多个作品的信息:</p>
<pre><code class="language-proto">syntax = &quot;proto3&quot;; // 指定版本

option go_package = &quot;example/proto&quot;; // 指定 Go 代码生成的包名

package artwork; // 包名, 用于区分不同的 proto 文件. 注意此包名并不是 Go 中的包名

// 定义消息类型
message ArtworkInfo {
    uint64 artworkID = 1; 
    string title = 2;
    SourceName source = 3; // 引用枚举类型
    repeated string tags = 4; // repeated 表示为数组
    bool r18 = 5;
    repeated PictureInfo pictures = 6; // 引用嵌套消息类型

    // 定义枚举类型
    enum SourceName {
        Pixiv = 0;
        Twitter = 1;
        // ...
    }

    // 定义嵌套消息类型
    message PictureInfo {
        uint64 pictureID = 1;
        uint64 width = 2;
        uint64 height = 3;
    }
}

message GetArtworkInfoRequest {
    uint64 artworkID = 1;
}

// 定义服务
service ArtworkService {
    // 定义方法
    rpc GetArtworkInfo (GetArtworkInfoRequest) returns (ArtworkInfo);
    rpc GetArtworkInfoList (GetArtworkInfoRequest) returns (stream ArtworkInfo); // 定义流式方法 //[!code ++]
}
</code></pre>
<h5>客户端流式方法</h5>
<p>与服务端流式方法相反, 客户端向数据流中写入并发送一系列消息, 服务端从流中读取这些消息, 直到没有消息为止, 然后返回一个响应消息.</p>
<p>在方法参数前加上 <code>stream</code> 关键字即可定义客户端流式方法.</p>
<h5>双向流式方法</h5>
<p>双向流式方法是指客户端和服务端都可以通过一个读写数据流来发送一系列消息. 这两个流独立运行, 因此客户端和服务端可以以任意顺序读写: 例如, 服务端可以在写入响应前等待接收所有客户端消息, 或者可以先读取一个消息, 然后再写入一个消息, 或者读取或写入多个消息, 甚至可以同时读写.</p>
<p>在方法参数和返回值前加上 <code>stream</code> 关键字即可定义双向流式方法.</p>
<h3>生成 Go 代码</h3>
<p>上文所述的内容都还是在 <code>proto</code> 文件中完成, 要想在 Go 中使用, 还需要生成 Go 代码.</p>
<p>在项目目录下执行:</p>
<pre><code class="language-bash">protoc --go_out=. --go-grpc_out=. example.proto
</code></pre>
<p>protoc 会在当前目录下生成 <code>proto/example.pb.go</code> 和 <code>proto/example_grpc.pb.go</code> 两个文件, 前者包含了 <code>example.proto</code> 中定义的消息类型, 后者包含了 <code>example.proto</code> 中定义的服务和方法.</p>
<h3>服务端实现</h3>
<p>实现上文定义的 <code>ArtworkService</code> 服务, 需要做两部分工作:</p>
<ol>
<li>实现 <code>ArtworkService</code> 服务中定义的所有方法. 即实现 <code>example_grpc.pb.go</code> 中的 <code>ArtworkServiceServer</code> 接口.</li>
<li>运行一个 gRPC 服务器, 监听来自客户端的请求并返回响应.</li>
</ol>
<h4>实现服务方法</h4>
<p>打开 <code>example_grpc.pb.go</code> 文件, 可以看到 <code>ArtworkServiceServer</code> 接口的定义:</p>
<pre><code class="language-go">// ArtworkServiceServer is the server API for ArtworkService service.
// All implementations must embed UnimplementedArtworkServiceServer
// for forward compatibility
type ArtworkServiceServer interface {
 GetArtworkInfo(context.Context, *GetArtworkInfoRequest) (*ArtworkInfo, error)
 GetArtworkInfoList(*GetArtworkInfoRequest, ArtworkService_GetArtworkInfoListServer) error
 mustEmbedUnimplementedArtworkServiceServer()
}
</code></pre>
<p>在项目中导入 <code>example/proto</code> 包, 并实现 <code>ArtworkServiceServer</code> 接口:</p>
<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;log&quot;
    &quot;net&quot;

    &quot;example/proto&quot;
    &quot;google.golang.org/grpc&quot;
)

type ArtworkServiceServer struct {
    proto.UnimplementedArtworkServiceServer // 为了实现向前兼容, 需要嵌入此结构体
}

func (s *ArtworkServiceServer) GetArtworkInfo(ctx context.Context, req *proto.GetArtworkInfoRequest) (*proto.ArtworkInfo, error) {
    // 实现
    return &amp;proto.ArtworkInfo{}, nil
}

func (s *ArtworkServiceServer) GetArtworkInfoList(req *proto.GetArtworkInfoRequest, stream proto.ArtworkService_GetArtworkInfoListServer) error {
    // 实现
    for i := 0; i &lt; 10; i++ {
        err := stream.Send(&amp;proto.ArtworkInfo{})
        if err != nil {
            return err
        }
    }
    return nil
}

var server = &amp;ArtworkServiceServer{}

func main() {
}
</code></pre>
<p>可以看到, 流式方法和一元方法的区别在于, 流式方法的参数或返回值是一个流, 需要向流中写入或读取消息.</p>
<pre><code class="language-go">func (s *ArtworkServiceServer) GetArtworkInfoList(req *proto.GetArtworkInfoRequest, stream proto.ArtworkService_GetArtworkInfoListServer) error {
    // 实现
    for i := 0; i &lt; 10; i++ {
        err := stream.Send(&amp;proto.ArtworkInfo{})
        if err != nil {
            return err
        }
    }
    return nil
}
</code></pre>
<h4>运行 gRPC 服务器</h4>
<p>在 <code>main</code> 函数中, 创建一个 gRPC 服务器, 并注册 <code>ArtworkServiceServer</code>:</p>
<pre><code class="language-go">func main() {
    lis, err := net.Listen(&quot;tcp&quot;, &quot;:8080&quot;)
    if err != nil {
        log.Fatalf(&quot;failed to listen: %v&quot;, err)
    }
    s := grpc.NewServer()
    proto.RegisterArtworkServiceServer(s, server)
    if err := s.Serve(lis); err != nil {
        log.Fatalf(&quot;failed to serve: %v&quot;, err)
    }
}
</code></pre>
<p>::: warning<br>
此处没有使用任何安全措施<br>
:::</p>
<h3>客户端调用</h3>
<p>在客户端调用 gRPC 服务, 只需要初始化连接, 并创建客户端, 然后直接调用定义的方法.</p>
<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;log&quot;

    &quot;example/proto&quot;
    &quot;google.golang.org/grpc&quot;
    &quot;google.golang.org/grpc/credentials/insecure&quot;
)

func main() {
    conn, err := grpc.Dial(&quot;localhost:8080&quot;, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf(&quot;did not connect: %v&quot;, err)
    }
    defer conn.Close()
    client := proto.NewArtworkServiceClient(conn)
    resp, err := client.GetArtworkInfo(context.Background(), &amp;proto.GetArtworkInfoRequest{ArtworkID: 1})
    if err != nil {
        log.Fatalf(&quot;could not get artwork info: %v&quot;, err)
    }
    log.Println(resp)
}
</code></pre>
<p>而对于流式方法, 客户端需要从流中读取响应消息, 直到没有消息为止.</p>
<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;log&quot;
    &quot;io&quot;

    &quot;example/proto&quot;
    &quot;google.golang.org/grpc&quot;
    &quot;google.golang.org/grpc/credentials/insecure&quot;
)

func main() {
    conn, err := grpc.Dial(&quot;localhost:8080&quot;, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf(&quot;did not connect: %v&quot;, err)
    }
    defer conn.Close()
    client := proto.NewArtworkServiceClient(conn)
    stream, err := client.GetArtworkInfoList(context.Background(), &amp;proto.GetArtworkInfoRequest{ArtworkID: 1})
    if err != nil {
        log.Fatalf(&quot;could not get artwork info: %v&quot;, err)
    }
    for {
        resp, err := stream.Recv()
        if err == io.EOF { // 判断是否已结束
            break
        }
        if err != nil {
            log.Fatalf(&quot;could not get artwork info: %v&quot;, err)
        }
        log.Println(resp)
    }
}
</code></pre>
<h2>参考</h2>
<ul>
<li><a href="https://grpc.io/docs/">gRPC Docs</a></li>
<li><a href="https://protobuf.dev/programming-guides/proto3/">Protocol Buffers Docs</a></li>
</ul>
<br/><p>访问 <a href="https://krau.top/posts/grpc-and-protobuf-in-go-start" target="_blank">https://krau.top/posts/grpc-and-protobuf-in-go-start</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-10-21T21:27:48.000Z</published>
        <rights>true</rights>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用conda安装和管理python多版本环境]]></title>
        <id>https://krau.top/posts/manage-python-version-by-conda</id>
        <link href="https://krau.top/posts/manage-python-version-by-conda"/>
        <updated>2023-04-14T14:09:59.000Z</updated>
        <content type="html"><![CDATA[<p><s>感觉不如编译安装</s></p>
<!-- more -->
<h2>conda</h2>
<p>conda 是一个 python 的环境管理器, 它包含在 <a href="https://www.anaconda.com/">Anaconda</a> 内, Anaconda 是专注于数据分析的Python发行版本.</p>
<p>即使你没有数据分析\科学计算的需求, 也可以使用 conda 这个强大的工具来管理 python 环境</p>
<p>如果你不想安装这么大一个东西, 可以看我上次发的使用 <a href="./install-manage-python-version-on-linux.md">venv + 编译安装的方式管理python环境</a></p>
<h2>安装</h2>
<p>去<a href="https://www.anaconda.com/products/distribution#Downloads">Anaconda官网</a>下载</p>
<p>或者你可以选择 <a href="https://docs.conda.io/en/latest/miniconda.html">Miniconda</a></p>
<pre><code class="language-shell">wget https://repo.anaconda.com/archive/Anaconda3-2023.03-Linux-x86_64.sh
</code></pre>
<p>下载完成后, 执行 <code>bash Anaconda3-2023.03-Linux-x86_64.sh</code> 即可开始安装</p>
<p>安装完成后, 重启终端, 即可使用 conda</p>
<h2>使用</h2>
<p>更新</p>
<pre><code class="language-shell">conda update --all 
</code></pre>
<p>创建虚拟环境</p>
<pre><code class="language-shell">conda create -n &lt;name&gt; python=&lt;version&gt;
</code></pre>
<p>-n 后是虚拟环境的名字, python=version 指定要使用的 python 版本</p>
<p>激活环境</p>
<pre><code class="language-shell">conda activate &lt;name&gt;
</code></pre>
<p>退出虚拟环境</p>
<pre><code class="language-shell">conda deactivate
</code></pre>
<p>删除虚拟环境</p>
<pre><code class="language-shell">conda env remove &lt;name&gt;
</code></pre>
<p>列出虚拟环境</p>
<pre><code class="language-shell">conda env list
</code></pre>
<p>列出当前环境的包</p>
<pre><code class="language-shell">conda list
</code></pre>
<p>安装包, 和 pip 一样</p>
<pre><code class="language-shell">conda install &lt;package&gt;
</code></pre>
<br/><p>访问 <a href="https://krau.top/posts/manage-python-version-by-conda" target="_blank">https://krau.top/posts/manage-python-version-by-conda</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-04-14T14:09:59.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Debian/Linux安装最新Python版本及多版本环境管理]]></title>
        <id>https://krau.top/posts/install-manage-python-version-on-linux/index</id>
        <link href="https://krau.top/posts/install-manage-python-version-on-linux/index"/>
        <updated>2023-04-13T15:27:27.000Z</updated>
        <content type="html"><![CDATA[<p>Python 的环境管理确实烦,而且会出现版本之间不兼容的情况,所以有必要知道如何安装不同版本的 Python 并管理其环境.</p>
<p>本文系统环境为 Debian11, 虚拟环境工具使用的是 venv, 采用编译安装的方法. Python 版本为 3.11.6</p>
<!-- more -->
<h2>安装</h2>
<h3>下载Python源码</h3>
<p>去往 <a href="https://www.python.org/downloads/">Python 官网</a> 下载你想安装的版本的源码, 以目前最新的 Python3.11 为例</p>
<p><img src="https://krau.top/assets/install-manage-python-version-on-linux_04-13-15-38-10.CmjkRwt1.webp" alt="图 3"></p>
<p>使用 wget 下载:</p>
<pre><code class="language-shell">wget https://www.python.org/ftp/python/3.11.6/Python-3.11.6.tar.xz
</code></pre>
<h3>配置构建</h3>
<p>下载完成后, 将其解压:</p>
<pre><code class="language-shell">tar -xf Python-3.11.6.tar.xz
</code></pre>
<p>安装必要的依赖软件:</p>
<pre><code class="language-shell">sudo apt update

sudo apt install build-essential zlib1g-dev libncurses5-dev \
libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev pkg-config \
libreadline-dev libffi-dev curl libbz2-dev curl libgdbm-dev \ 
lzma-dev uuid-dev libsqlite3-dev openssl tk8.6-dev -y
</code></pre>
<p>进入 Python 源码文件夹, 并配置</p>
<pre><code class="language-shell">cd Python-3.11.6
./configure --enable-optimizations --with-lto --prefix=/usr/local/python3.11
</code></pre>
<p>::: tip 注意<br>
<code>configure</code> 后的 <code>--enable-optimizations</code> <code>--with-lto</code> 两个参数是为了提高性能, 但它会使得 Python 编译过程变慢, 你可以选择不启用它们. 如果你遇到了问题, 可以尝试只开启 <code>--enable-optimizations</code>, 或者二者都不使用</p>
<p>关于配置参数的更多信息, 可以参阅<a href="https://docs.python.org/zh-cn/3/using/configure.html">官方文档</a><br>
:::</p>
<p>如果上面命令的输出没有错误, 那么就可以开始编译了.</p>
<p><img src="https://krau.top/assets/install-manage-python-version-on-linux_04-13-15-52-08.CRoYJil3.webp" alt="图 4"></p>
<p>若提示缺少某个依赖, 请手动安装它</p>
<h3>编译安装</h3>
<p>运行 <code>make</code> 即可开始构建 Python , 若你有多个 cpu 核心, 可以加上 <code>-j</code> 参数来多核编译, <code>-j</code> 后是你的 cpu 核心数, 可以使用 <code>nproc</code> 获取</p>
<p>若你机器的性能不佳, 这一过程可能十分漫长(几十分钟甚至更久), 你可以使用 screen 或 tmux 使其在后台运行</p>
<p><img src="https://krau.top/assets/install-manage-python-version-on-linux_04-13-15-57-12.oaOoO5s9.webp" alt="图 5"></p>
<p>完成后, 使用 <code>sudo make altinstall</code> 安装(可能也很漫长,但比上一步要快), 之后你就可以使用 Python3.11:</p>
<p><img src="https://krau.top/assets/install-manage-python-version-on-linux_04-13-16-21-43.7Wjx4pdS.webp" alt="图 6"></p>
<pre><code class="language-shell">python3.11 --version
</code></pre>
<p><img src="https://krau.top/assets/install-manage-python-version-on-linux_04-13-16-25-55.B2lCiw7N.webp" alt="图 7"></p>
<h2>环境管理</h2>
<h3>虚拟环境</h3>
<h4>创建</h4>
<p>Python 的虚拟环境可以将项目与全局环境相互隔离,可以避免污染你的系统环境,并方便得使多个 Python 版本共存与切换.</p>
<p>本文使用的虚拟环境工具是 <a href="https://docs.python.org/3/library/venv.html">venv</a> , 它是 Python 官方推荐的工具.</p>
<blockquote>
<p>但其实更现代的工具是 <a href="https://python-poetry.org/">poetry</a> , 然而其使用比较复杂,且主要面向 pypl 模块开发者, 如有兴趣你可以自行了解.</p>
</blockquote>
<p>安装 venv:</p>
<pre><code class="language-shell">apt install python3-venv
</code></pre>
<p>使用以下命令在当前目录下创建虚拟环境:</p>
<pre><code class="language-shell">python3.11 -m venv myenv
</code></pre>
<p>这个命令会在你当前目录下创建 <code>myenv</code> 文件夹, 它就是一个&quot;虚拟环境&quot;, 其中包含了你所指定的 <code>Python3.11</code> 版本, 未来在虚拟环境中安装的第三方库等文件也会在此文件夹内.</p>
<p>显然, 如果你想创建一个其他 Python 版本的虚拟环境, 那么只需要将前面的 <code>python3.11</code> 改成你需要的版本且已经安装的即可</p>
<h4>使用</h4>
<p>创建完成后,要进入到虚拟环境,需要激活它,使用以下命令.</p>
<pre><code class="language-shell">source myenv/bin/activate
</code></pre>
<p>这时你的终端应该会有一些标识来提示你处在虚拟环境中,如下</p>
<p><img src="https://krau.top/posts/install-manage-python-version-on-linux/install-manage-python-version-on-linux_04-13-16-54-47.webp" alt="图 10"></p>
<p>在虚拟环境中, <code>python</code> <code>python3.11</code> <code>python3</code> 命令都可以打开 python ,并且版本都是 python3.11</p>
<p>在其中使用 pip 安装的包也都在虚拟环境中</p>
<p>使用 <code>deactive</code> 命令可退出虚拟环境</p>
<h4>删除</h4>
<p>使用 venv 工具创建的虚拟环境实际上就是一个文件夹, 删除它就是删除了环境</p>
<h3>默认版本</h3>
<p>始终建议使用官方软件源提供的 python 作为默认版本, 如果你真的想要替换默认版本, 那么可以使用下面的命令</p>
<pre><code class="language-shell">sudo ln -sf /usr/bin/python3.11 /usr/bin/python
</code></pre>
<p>这条命令将 python3.11 的可执行文件链接到 linux PATH 的默认搜索路径中, 此时系统的默认版本就是 3.11</p>
<p>::: tip<br>
即使默认版本就是你想要使用的 Python 版本, 你也应该在你的项目里创建虚拟环境, 除非你想把自己的系统环境搞得乱七八糟<br>
:::</p>
<br/><p>访问 <a href="https://krau.top/posts/install-manage-python-version-on-linux/index" target="_blank">https://krau.top/posts/install-manage-python-version-on-linux/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-04-13T15:27:27.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[JWT 认证及其在 FastAPI 中的使用]]></title>
        <id>https://krau.top/posts/fastapi-jwt/index</id>
        <link href="https://krau.top/posts/fastapi-jwt/index"/>
        <updated>2023-03-22T10:59:53.000Z</updated>
        <content type="html"><![CDATA[<h2>什么是 JWT</h2>
<p>JWT: JSON Web Tokens,它是一种将 JSON 对象编码为没有空格，且难以理解的长字符串的标准。在具体上,它就是一段字符串,下面就是 FastAPI 文档中给出的例子</p>
<pre><code class="language-txt">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
</code></pre>
<p>抽象地把它看成是这样的</p>
<pre><code class="language-txt">aaaaaaaaaaaa.bbbbbbbbbbbb.cccccccccccc
</code></pre>
<!-- more -->
<p>jwt 由三个部分组成,这三个部分之间用点<code>.</code>拼接起来,这三个部分分别是</p>
<ul>
<li>Header --- 头,定义了 jwt 使用的算法等信息</li>
<li>Payload --- 载荷,存储有效信息的地方</li>
<li>Signature --- 签名,用于校验 jwt 令牌</li>
</ul>
<p>这三个部分中,只有第三个部分是无法还原的,前两个部分只是简单的进行了 base64 编码,把上面例子中的前两部分解码之后,看起来是这样的:</p>
<p>Header:</p>
<pre><code class="language-json">{
    &quot;alg&quot;:&quot;HS256&quot;,
    &quot;typ&quot;:&quot;JWT&quot;
}

</code></pre>
<p>alg 即为算法,这里使用了 HS256 算法</p>
<p>typ 即为类型,指明这里是一个 JWT 字符串</p>
<p>Payload</p>
<pre><code class="language-json">{
    &quot;sub&quot;:&quot;1234567890&quot;,
    &quot;name&quot;:&quot;John Doe&quot;,
    &quot;iat&quot;:1516239022
}
</code></pre>
<p>可以看到 Payload 中存储了主要的信息</p>
<p>::: tip 提示<br>
根据 JWT 规范, Payload 中的 <code>sub</code> 是一个预定义的声明（Claim），表示JWT的主题（<strong>Subject</strong>）。它指定了JWT所代表的实体或主题，通常是用户或客户端应用程序。<br>
<code>sub</code>声明的值是字符串类型，通常是<strong>唯一</strong>标识用户或客户端应用程序的ID。<br>
:::</p>
<p>而 Signature 则是通过加盐加密得到的,加密由服务端实现</p>
<p>::: tip 关于具体如何实现</p>
<p>参考下图</p>
<p><img src="https://krau.top/assets/fastapi-jwt_03-22-11-28-26.D9njXLCP.webp" alt="图 1"></p>
<p>即,把 header 和 payload 部分进行 base64编码,用点拼接起来,得到<strong>结果</strong>,把这个结果用我们设置的密钥和算法进行加盐加密,就得到了 Signature</p>
<p>:::</p>
<h3>JWT 有什么用</h3>
<p>其实它与 token ,cookie 的作用类似,就是用来免密码认证客户端</p>
<p>比如你去上学,学校是服务端,你是客户端.你完成学籍注册之后,学校发给你一个学生证,学生证就是你的 jwt 令牌.</p>
<p>你使用学校的很多服务,都要出示学生证.</p>
<p>而在实际业务中,jwt 令牌是有过期时间的,过期之后,需要客户端再次登录,获取新的 jwt</p>
<h2>FastAPI 实现 Bearer JWT 令牌验证</h2>
<p>需要安装以下几个包</p>
<p>python-jose，在 Python 中生成和校验 JWT 令牌</p>
<pre><code class="language-bash">pip install python-jose[cryptography]
</code></pre>
<p>Passlib ,处理密码哈希</p>
<pre><code class="language-bash">pip install passlib[bcrypt]
</code></pre>
<p>::: warning</p>
<p>密码哈希和 jwt 的加密不是同一个问题.</p>
<p>哈希是指把特定内容（本例中为密码）转换为乱码形式的字节序列（其实就是字符串）。</p>
<p>每次传入完全相同的内容时（比如，完全相同的密码），返回的都是完全相同的乱码。</p>
<p>但这个乱码无法转换回传入的密码。</p>
<p>把密码进行哈希并存储是必要的安全措施</p>
<p>:::</p>
<p>先不考虑用户注册的问题,假设我们已经有了一个在数据库中的用户,那么这里的逻辑是这样的:</p>
<ol>
<li>该用户通过用户名和密码登录</li>
<li>服务端对其进行校验和认证</li>
<li>完成认证后,用户拿到 jwt 令牌(token)</li>
<li>用户使用 jwt 令牌(token)访问其他服务</li>
</ol>
<p>所以,我们需要:</p>
<ol>
<li>一个存储了用户信息的数据库,其中密码存储的是哈希值</li>
<li>用户和 jwt 令牌(token)的模型</li>
<li>用于从数据库中获得用户信息,并验证的函数. 包括获取用户,哈希传入的密码,验证用户等</li>
<li>用于创建 jwt 令牌的函数</li>
<li>给用户登录并获取 jwt 令牌(token)的接口</li>
<li>验证用户的 jwt 是否正确和有效</li>
</ol>
<h3>数据库与模型</h3>
<p>呃,为了简便和偷懒,就用 FastAPI 文档中的假数据库吧</p>
<pre><code class="language-python">fake_users_db = {
    &quot;johndoe&quot;: {
        &quot;username&quot;: &quot;johndoe&quot;,
        &quot;full_name&quot;: &quot;John Doe&quot;,
        &quot;email&quot;: &quot;johndoe@example.com&quot;,
        &quot;hashed_password&quot;: &quot;$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW&quot;,
        &quot;disabled&quot;: False,
    }
}
</code></pre>
<p>定义之后要用到的模型</p>
<pre><code class="language-python">from pydantic import BaseModel
from typing import Union

class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: Union[str, None] = None


class User(BaseModel):
    username: str
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None


class UserInDB(User):
    hashed_password: str

</code></pre>
<p><code>Token</code> 模型用于在登录接口中指定 <code>response_model</code></p>
<p><code>TokenData</code> 用于从 token 中获取数据</p>
<p><code>User</code> 即用户模型,供外部使用</p>
<p><code>UserInDB</code> 是用户在数据库中的模型,它继承自 <code>User</code> 添加了 <code>hashed_password</code> 属性</p>
<p>这些模型在之后会用到</p>
<h3>对用户名和密码的校验与认证</h3>
<p>这部分比较简单,就是把用户输入的密码哈希之后和数据库中的对比就好了</p>
<pre><code class="language-python">from passlib.context import CryptContext
pwd_context = CryptContext(schemes=[&quot;bcrypt&quot;], deprecated=&quot;auto&quot;)

def verify_password(plain_password, hashed_password):
    &quot;&quot;&quot;
    校验密码是否正确
    将传入的明文密码与数据库中的哈希值进行比对
    &quot;&quot;&quot;
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password):
    &quot;&quot;&quot;获取传入的密码的哈希值&quot;&quot;&quot;
    return pwd_context.hash(password)


def get_user(db, username: str):
    &quot;&quot;&quot;根据用户名从指定数据库中获取用户&quot;&quot;&quot;
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


def authenticate_user(fake_db, username: str, password: str):
    &quot;&quot;&quot;身份验证, 返回用户对象或者 False&quot;&quot;&quot;
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user
</code></pre>
<h3>创建 jwt token</h3>
<p>完成认证之后,就可以给用户创建 token 了</p>
<p>首先,定义 jwt 的加密密钥,加密算法和过期时间</p>
<pre><code class="language-python">SECRET_KEY = &quot;09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7&quot;
ALGORITHM = &quot;HS256&quot;
ACCESS_TOKEN_EXPIRE_MINUTES = 30
</code></pre>
<p>::: warning 注意</p>
<p>在实际项目中,jwt 的密钥不应该直接写在代码中,而是应该从环境变量中读取</p>
<p>例如,从环境变量中获取 SECRET_KEY</p>
<pre><code class="language-python">import os
SECRET_KEY = os.environ.get(&quot;SECRET_KEY&quot;)
</code></pre>
<p>:::</p>
<p>然后写创建 token 的函数</p>
<pre><code class="language-python">from datetime import datetime, timedelta
from jose import JWTError, jwt

def create_access_token(data: dict, expires_delta=None):
    &quot;&quot;&quot;创建 jwt token&quot;&quot;&quot;
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({&quot;exp&quot;: expire})
    # 注意这里就用到了上面定义的 SECRET_KEY 和 ALGORITHM
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt
</code></pre>
<p>上面这个函数把传入的用于获取 jwt 的数据进行拷贝,并添加过期时间,然后 encode 得到 jwt token 并返回</p>
<p>至此,我们完成了对用户的校验和认证,以及创建 jwt token 的功能,下面我们要在接口中使用它们</p>
<h3>提供登录并创建token的接口</h3>
<pre><code class="language-python">from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;token&quot;)
@app.post(&quot;/token&quot;, response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    &quot;&quot;&quot;登录并获取 token&quot;&quot;&quot;
    # 通过用户名和密码获取用户, 如果用户不存在或者密码错误, 则抛出异常
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)

    if not user:
        raise HTTPException(
            # 这部分是规范, 401 表示未授权
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=&quot;Incorrect username or password&quot;,
            headers={&quot;WWW-Authenticate&quot;: &quot;Bearer&quot;},
        )

    # 上面已经相当于进行了用户身份验证, 所以这里可以直接创建 token
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={&quot;sub&quot;: user.username}, expires_delta=access_token_expires
    )
    # 这里如何返回也是规范, 我们已在上面已经定义好了 response_model=Token
    return {&quot;access_token&quot;: access_token, &quot;token_type&quot;: &quot;bearer&quot;}
</code></pre>
<p><code>oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;token&quot;)</code> 声明了用户应该从哪个路由获取 token (相对路径),但是并未实现具体的功能</p>
<p>而我们自己写的 <code>login_for_access_token</code> 则是实现.</p>
<p>现在用户成功登录后,就拿到了 jwt token ,访问其他服务时,浏览器会携带 jwt token 一起发送请求</p>
<p>我们只需要在其他接口中,对用户传来的 jwt token 进行验证即可</p>
<h3>在其他接口中验证jwt token</h3>
<p>比如我们有一个获取用户信息的接口 <code>read_users_me</code> ,那么,我们需要:</p>
<ol>
<li>获取用户传来的 token</li>
<li>验证 token 是否正确和有效</li>
<li>通过 token 获取当前的用户信息</li>
</ol>
<p>首先,解密 jwt token ,获取当前用户:</p>
<pre><code class="language-python">async def get_current_user(token: str = Depends(oauth2_scheme)):
    &quot;&quot;&quot;依赖函数, 用于获取当前用户&quot;&quot;&quot;
    # 定义一个异常, 用于在用户身份验证失败时抛出
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=&quot;Could not validate credentials&quot;,
        headers={&quot;WWW-Authenticate&quot;: &quot;Bearer&quot;},
    )

    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        # 从 payload 中获取 username
        # 关于 sub 的解释, 可以参考文档
        username: str = payload.get(&quot;sub&quot;)
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
    &quot;&quot;&quot;依赖函数, 用于获取当前活跃的用户,用到了上面的依赖函数 get_current_user
    为什么 current_user 可以是 User 类型而不是 get_current_user 返回的 UserInDB 类型呢?
    因为 UserInDB 类型是 User 类型的子类, 所以可以直接赋值给 User 类型的变量
    &quot;&quot;&quot;
    if current_user.disabled:
        raise HTTPException(status_code=400, detail=&quot;Inactive user&quot;)
    return current_user
</code></pre>
<p>在 数据库与模型 中, 我们定义的 <code>TokenData</code> 就是这里要用的</p>
<p>因为只是演示,所以这个 <code>TokenData</code> 类显得有点多此一举.</p>
<p>但是如果你需要传递多个 JWT 数据，或者有多个函数需要访问 JWT 数据，那么 <code>TokenData</code> 类可以方便地将这些数据打包在一起，并在函数之间传递它们。</p>
<p>之后就可以在接口中利用依赖注入,验证用户请求</p>
<pre><code class="language-python">@app.get(&quot;/users/me/&quot;, response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    &quot;&quot;&quot;获取当前用户信息&quot;&quot;&quot;
    return current_user
</code></pre>
<h2>总结</h2>
<p>JWT 是一种认证方式,它的本质是一段带有签名的字符串.</p>
<p>在 Python 中,我们可以使用 <code>jwt</code> 模块来创建和解析 jwt token.</p>
<p>在 FastAPI 中,我们可以使用 <code>OAuth2PasswordBearer</code> 来获取用户传来的 token,然后使用 <code>jwt</code> 模块来解析 token,从而获取用户信息.</p>
<h2>参考</h2>
<p><a href="https://jwt.io/introduction/">JWT 官方文档</a></p>
<p><a href="https://fastapi.tiangolo.com/tutorial/security/">FastAPI 官方文档 - 安全性</a></p>
<p><a href="https://fastapi.tiangolo.com/tutorial/dependencies/">FastAPI 官方文档 - 依赖注入</a></p>
<br/><p>访问 <a href="https://krau.top/posts/fastapi-jwt/index" target="_blank">https://krau.top/posts/fastapi-jwt/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-03-22T10:59:53.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[ArchLinux on Y9000P2022十分好用,就是有点难用]]></title>
        <id>https://krau.top/posts/archlinux-on-y9kp22-1</id>
        <link href="https://krau.top/posts/archlinux-on-y9kp22-1"/>
        <updated>2023-02-20T21:51:21.000Z</updated>
        <content type="html"><![CDATA[<p>受不了啦，给笔记本装上 Arch 折磨自己！</p>
<!-- more -->
<h2>起因</h2>
<p>突然想安装 Arch 的理由：</p>
<ol>
<li>酷！不觉得这很酷吗，做为一个理科生我觉得用一个完全手动安装的Linux发行版真的太酷了，很符合我对带黑客的想象！</li>
<li>自由！不同于乌班图、opensuse、centos这些具有实体维护者（商业公司）的发行版，Arch是由社区驱动的，属于自由阵营！不会受到各种奇怪法律的管辖！</li>
<li>简洁！Arch连安装的gui图形界面都没有，需要什么都要自己手动安装，最后的系统组件和应用将会不多也不少，符合自己的需求。</li>
<li>软件丰富！绝大多数软件已经有其他人帮你打包好了，安装就是一条命令解决！</li>
<li>不想用win了！但是不想用乌班图，太臃肿了！也不想用debian，太土了！</li>
<li>编不动了</li>
</ol>
<p>电脑是拯救者Y9000P2022款，配置：</p>
<ul>
<li>CPU：I7 12700H</li>
<li>GPU：RTX3060M</li>
</ul>
<h2>关于如何安装</h2>
<p>很简单，查 <a href="https://wiki.archlinux.org/">Archlinux官方wiki</a> 就可以了。嫌官方的不够友好的话可以看这个：<a href="https://arch.icekylin.online/">ArchLinux简明指南</a></p>
<h2>遇到的问题</h2>
<h3>Nvidia显卡驱动</h3>
<p>SoNvidiaFUCKYOU!<br>
折腾了很长时间，这n卡的驱动就是装不上，最后是重装了系统，然后安装开源版驱动解决了：</p>
<p><code>sudo pacman -S nvidia-open</code></p>
<p>安装之后的处理和wiki里是一样的，好像比较新的型号的显卡（30系）都是要安装这个驱动。</p>
<p>这个开源版并不是社区的那个逆向出来的开源版，而是nvidia官方给的，性能是没有太大问题的</p>
<h3>Nvidia对wayland的兼容性</h3>
<p>SoNvidiaFUCKYOU!</p>
<p>n卡+linux桌面=shit!N卡用户建议暂时不要用wayland，老老实实Xorg，兼容性比wayland好太多了。</p>
<h3>多显示器无法正常使用</h3>
<p>简单来说这还是N卡的锅，各种折腾尝试有可能会解决，玄学就是了。</p>
<p>我能正常使用多显示器的情景有两个：</p>
<ol>
<li>BIOS设置混合模式，安装 <code>optimus-manager</code> 开核显模式。</li>
<li>BIOS开独显直连，把nvidia驱动写进早加载，不要安装 <code>optimus-manager</code>。</li>
</ol>
<p>另外这两种模式都是Xorg，nvidia对wayland真的没啥兼容性，会出现一堆奇怪问题。</p>
<h3>蓝牙耳机无法连接</h3>
<p>安装这个就可以了</p>
<p><code>sudo pacman -S bluez bluez-utils pulseaudio-bluetooth</code></p>
<h2>体验</h2>
<p>太好用啦！所有软件都用pacman或者yay直接安装，并且能纳入滚动升级中。</p>
<h3>聊天通讯</h3>
<p>Telegram：字体有问题，安装了几个中文字体之后正常了，体验和win是一样的，tg官方对于linux平台也是下了精力的。实在不行还能用web端。</p>
<p>QQ：你没事吧？跟张小龙学会了登个号还得手机扫码？口区。而且登录的时候还会提示安全风险，难绷，不愧是腾讯。</p>
<h3>浏览器</h3>
<p>火狐，很不错。但是缩放有时候会出问题，打开的时候界面突然很小或者很大...应该是我多显示器的原因。</p>
<p>然后还试了试 brave，只能说好像没多好用，而且一股子去中心化web3区块链虚拟币的味道。</p>
<h3>游戏</h3>
<p>出乎意料的流畅啊！谁说的linux打游戏是ppt的，体验和win没感知上的差距。当然可能是我不玩什么3a大作（实际上来说我几乎不玩游戏了），玩了 muse dash 这个音游试了下，体验真的和win没什么区别。</p>
<h3>开发与运维</h3>
<p>终端软件用的是 Termius（学生包白嫖的不用白不用），IDE还是vscode。</p>
<p>好用！</p>
<hr>
<p>快来加入Arch邪教吧！</p>
<p>顺便，Arch的图标真的是太好看啦！</p>
<p><img src="https://s2.loli.net/2023/02/20/C2uoFdGhVQZrwvb.png" alt="图片.png"></p>
<br/><p>访问 <a href="https://krau.top/posts/archlinux-on-y9kp22-1" target="_blank">https://krau.top/posts/archlinux-on-y9kp22-1</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-02-20T21:51:21.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[为 valaxy 博客 yun 主题添加 artitalk 说说页面]]></title>
        <id>https://krau.top/posts/add-artitalk-for-valaxy-yun</id>
        <link href="https://krau.top/posts/add-artitalk-for-valaxy-yun"/>
        <updated>2023-01-20T00:53:46.000Z</updated>
        <content type="html"><![CDATA[<h2>Artitalk</h2>
<p><a href="https://artitalk.js.org/">Artitalk</a> 是一个基于 Leancloud 的简单说说 js，具体参见官方文档。</p>
<p>我成功将它接入了我的博客，<a href="https://krau.top/about">点这里看看</a>。下面简单介绍一下接入方法。</p>
<blockquote>
<p>由于构建上的一些问题，我把它暂时移除了</p>
</blockquote>
<h2>注册并配置 Leancloud</h2>
<p>这一步与官方文档完全相同，<a href="https://artitalk.js.org/doc.html#%F0%9F%8C%88-leancloud-%E7%9A%84%E7%9B%B8%E5%85%B3%E5%87%86%E5%A4%87">点击查看</a></p>
<h2>配置 valaxy</h2>
<p>在博客根目录下，新建 index.heml，写入以下内容</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-scale=1.0&quot;&gt;
    &lt;link rel=&quot;icon&quot; href=&quot;favicon.ico&quot;&gt;
    &lt;title&gt;&lt;%= htmlWebpackPlugin.options.title %&gt;&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;noscript&gt;
      &lt;strong&gt;We're sorry but &lt;%= htmlWebpackPlugin.options.title %&gt; doesn't work properly without JavaScript enabled. Please enable it to continue.&lt;/strong&gt;
    &lt;/noscript&gt;
    &lt;div id=&quot;app&quot;&gt;&lt;/div&gt;
      &lt;script src=&quot;https://cdn.jsdelivr.net/npm/artitalk&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>然后在 /components/ 目录下，新建 Artitalk.vue ，写入以下内容</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;div id=&quot;artitalk_main&quot; /&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  mounted() {
    function addScript(url) {
      var s = document.createElement(&quot;script&quot;);
      s.id = &quot;at&quot;;
      url.indexOf(&quot;appId&quot;) == -1 ? (s.src = url) : (s.innerHTML = url);
      document.head.appendChild(s);
    }
    addScript(`
      new Artitalk({
          appId: '你的appid',
          appKey: '你的key',
          shuoPla: '',
          bgImg: 'https://cdn.jsdelivr.net/gh/drew233/cdn/20200409110727.webp',
          atEmoji: {
              huaji: 'https://cdn.jsdelivr.net/gh/moezx/cdn@3.1.9/img/Sakura/images/smilies/icon_huaji.gif',
              baiyan: 'https://cdn.jsdelivr.net/gh/Artitalk/Artitalk-emoji/baiyan.png',
              bishi: 'https://cdn.jsdelivr.net/gh/Artitalk/Artitalk-emoji/bishi.png',
              bizui: 'https://cdn.jsdelivr.net/gh/Artitalk/Artitalk-emoji/bizui.png',
              chan: 'https://cdn.jsdelivr.net/gh/Artitalk/Artitalk-emoji/chan.png'
          },
      })
    `);
  },
  destroyed() {
    document
      .querySelectorAll(&quot;#at&quot;)
      .forEach(element =&gt; element.parentNode.removeChild(element));
    delete window.AV;
  }
};
&lt;/script&gt;
</code></pre>
<p>这样就可以啦。在你想要显示说说的页面(.md文件)里，写上<code>&lt;Artitalk /&gt;</code> 就可以显示了。</p>
<h2>问题</h2>
<p>目前 Artitalk 只支持以 Leancloud 作为数据库，并且 cdn 用的还是 jsdeliver，速度感人。我提了 issue 希望能支持更多部署方式，不过看起来作者已经失踪有一段时间了...</p>
<br/><p>访问 <a href="https://krau.top/posts/add-artitalk-for-valaxy-yun" target="_blank">https://krau.top/posts/add-artitalk-for-valaxy-yun</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-01-20T00:53:46.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用ShareX和Markdown Image自动压缩截图为WebP并一键插入Markdown文档]]></title>
        <id>https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/index</id>
        <link href="https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/index"/>
        <updated>2023-01-16T18:04:11.000Z</updated>
        <content type="html"><![CDATA[<p>之前我曾安利过 <a href="http://localhost:4859/posts/pasteimage-in-vs">PasteImage</a> 这个插件，用 VSCode 写博客/文档的时候，用它可以很快捷地在md文档中插入图片。</p>
<p>近期的文章内的图片都已经压缩成了 webp 格式。但是 PasteImage 却不支持 webp 格式，于是我又想办法整出了今天这个解决方案，实现完全自动化的 压缩+插入图片。</p>
<!-- more -->
<h2>配置 ShareX</h2>
<p><a href="https://getsharex.com/">sharex</a> 是一个强大的截屏、录屏、分享软件，开源免费，支持高度自定义的自动化，</p>
<p>然后还要安装 <a href="https://ffmpeg.org/">ffmpeg</a> ，它在本文的作用是通过命令调用来压缩图片。</p>
<p>然后在 sharex 的动作设置中，添加一个动作</p>
<p><img src="https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/1.webp" alt="图 2"></p>
<ul>
<li>名称: 随意</li>
<li>文件路径: ffmpeg 的安装路径，要指定到 ffmpeg 可执行文件</li>
<li>参数: <code>-i &quot;$input&quot; -q 75 &quot;$output&quot;</code>  其中 -q 75 是以75%质量压缩</li>
<li>输出文件扩展名: webp</li>
<li>扩展名筛选: png</li>
</ul>
<p>勾上下面的 隐藏窗口 和 删除输入文件</p>
<p><img src="https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/2.webp" alt="图 3"></p>
<p>然后在截图后的任务里选中这些项(添加图像特效可视情况自行添加)</p>
<p><img src="https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/3.webp" alt="图 4"></p>
<h2>配置 Markdown Image</h2>
<p><a href="https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image">Markdown Image</a> 是一个 VSCode 插件，支持将剪贴板中的图片保存在指定路径下(或指定图床中)，并自动生成 markdown 格式的引用。</p>
<p>我的博客的文章图片是按照 <code>/public/photo/post-pics/文章名/文章名_时间</code> 的格式和路径保存在本地的，所以按照下面这样配置</p>
<pre><code class="language-json">&quot;markdown-image.base.fileNameFormat&quot;: &quot;${mdname}/${mdname}_${MM}-${DD}-${HH}-${mm}-${ss}&quot;,
&quot;markdown-image.local.path&quot;: &quot;../../public/photo/post-pics/&quot;,
&quot;markdown-image.local.referencePath&quot;: &quot;/photo/post-pics/${mdname}/&quot;,
</code></pre>
<p>更详细的配置，参照 <a href="https://github.com/imlinhanchao/vsc-markdown-image/blob/HEAD/README.zh-cn.md">Markdown Image 的官方文档</a></p>
<p>这样就完成啦。现在使用 sharex 截屏时会自动调用 ffmpeg 将文件压缩为 webp 格式并保存在剪贴板中，然后在 VSCode 中使用 <code>Alt + Shift + V</code> 粘贴即可。</p>
<br/><p>访问 <a href="https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/index" target="_blank">https://krau.top/posts/capture-screen-compress-to-webp-and-insert-markdown/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-01-16T18:04:11.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[将 hexo 博客迁移至 valaxy]]></title>
        <id>https://krau.top/posts/hexo-migrate-to-valaxy/index</id>
        <link href="https://krau.top/posts/hexo-migrate-to-valaxy/index"/>
        <updated>2023-01-16T09:56:55.000Z</updated>
        <content type="html"><![CDATA[<p>::: warning 注意<br>
本文章作于 valaxy 版本 0.12.2 , 部分内容可能已经过时 , 请结合<a href="https://valaxy.site/guide/getting-started">官方文档</a>一起阅读<br>
:::</p>
<h2>关于 valaxy</h2>
<p>这是一个各种意义上很新的东西，它的目标是成为下一代静态博客框架。</p>
<p>更多关于 valaxy 的介绍:</p>
<ul>
<li>
<p>项目地址：<a href="https://github.com/YunYouJun/valaxy">YunYouJun/valaxy</a></p>
</li>
<li>
<p>官方文档：<a href="https://valaxy.site/">valaxy</a></p>
</li>
</ul>
<p>但是 valaxy 并不旨在成为下一代的 hexo ，也就是说并不向着与 hexo 的兼容的方向开发。因此迁移起来并不是特别简单。</p>
<!-- more -->
<hr>
<h2>创建 valaxy 项目</h2>
<h3>环境</h3>
<p>Valaxy 要求 Node.js 的 14.18 以上，如果你是从 hexo 的较新版本迁移的，一般不需要去升级。</p>
<p>valaxy 建议使用 pnpm 作为包管理器，使用 <code>npm i -g pnpm</code> 全局安装它</p>
<h3>创建</h3>
<p>在工作文件夹下，使用以下命令</p>
<pre><code class="language-shell">pnpm create valaxy
</code></pre>
<p>然后输入你的博客文件夹名，同时也是项目名称，默认为 <code>valaxy-blog</code>。我这里使用 <code>valaxy-demo</code></p>
<p><img src="https://krau.top/assets/hexo-migrate-to-valaxy_01-16-13-37-38.BrOGu2kK.webp" alt="输入项目名称"></p>
<p>随后询问是否现在安装并启动，确定。</p>
<p><img src="https://krau.top/posts/hexo-migrate-to-valaxy/hexo-migrate-to-valaxy_01-16-13-39-16.webp" alt="询问是否安装并启动"></p>
<p>很快就完成了项目创建，此时可以在终端中按 o 打开浏览器预览</p>
<p><img src="https://krau.top/assets/hexo-migrate-to-valaxy_01-16-13-40-50.CUKkgTyM.webp" alt="预览"></p>
<p>进入创建好的项目文件夹，使用以下命令安装依赖</p>
<pre><code class="language-shell"># install
pnpm i
</code></pre>
<h3>命令</h3>
<p>valaxy 在默认局部配置脚本下的一些命令</p>
<p>开启本地开发环境（预览，支持热重载）</p>
<pre><code class="language-shell">pnpm run dev
</code></pre>
<p>构建静态文件</p>
<pre><code class="language-shell">pnpm run build
</code></pre>
<p>升级(pnpm 交互升级命令)</p>
<pre><code class="language-shell"># upgrade valaxy
pnpm up --latest -i
</code></pre>
<p>新建文章</p>
<pre><code class="language-shell">pnpm new post-title
</code></pre>
<h2>修改配置</h2>
<p>在此简要了解一下<a href="https://valaxy.site/guide/getting-started#%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84">目录结构</a>，见下图</p>
<p><img src="https://krau.top/assets/hexo-migrate-to-valaxy_01-16-14-13-22.DHccjA8t.webp" alt="valaxy目录结构"></p>
<p>其中 <code>valaxy.config.ts</code> 是主要需要修改的配置。建议使用 VSCode 编辑它，并按照提示安装推荐的插件。</p>
<blockquote>
<p>valaxy.config.ts 提供了完备的类型提示，这意味着你在 VSCode 中可以直接鼠标悬浮查看各参数注释。</p>
</blockquote>
<p>默认有些配置项未在 valaxy.config.ts 中给出，使用的是默认值，可以自己添加上就可以覆盖默认值。</p>
<h3>站点与作者信息</h3>
<p><img src="https://krau.top/assets/hexo-migrate-to-valaxy_01-16-13-50-34.x9E2uDVK.webp" alt="站点与作者信息配置"></p>
<ul>
<li>url: '站点链接'</li>
<li>lang: '默认语言(valaxy支持单页国际化)'</li>
<li>title: '站点标题'</li>
<li>subtitle: '副标题'</li>
<li>author:
<ul>
<li>name: '博主名字'</li>
<li>avatar: '头像地址'</li>
<li>status:
<ul>
<li>emoji: '一个emoji'</li>
<li>message: '状态说明'</li>
</ul>
</li>
</ul>
</li>
<li>mode: '默认颜色模式'</li>
<li>lastUpdated: '开启最近更新:true'</li>
<li>favicon: '站点图标地址'</li>
<li>description: '描述'</li>
</ul>
<p>示例:</p>
<pre><code class="language-ts">  url: 'http://krau.top',
  lang: 'zh-CN',
  title: '柯罗krau的博客 | krau\'s blog',
  subtitle: '',
  author: {
    name: '柯罗krau',
    avatar: '/photo/avatar/avatar.jpg',
    status:
    {
      emoji: '😐',
      message: '真不想上学'
  }},
  mode: 'auto',
  lastUpdated: true,
  favicon: '',
  description: '年轻，就要折腾',
  //...
</code></pre>
<h3>社交链接</h3>
<ul>
<li>social
<ul>
<li>name: '名称'</li>
<li>link: '链接'</li>
<li>icon: '图标'</li>
<li>color: '颜色'</li>
</ul>
</li>
</ul>
<p>示例:</p>
<pre><code class="language-ts">social: [
    {
      name: 'RSS',
      link: '/atom.xml',
      icon: 'i-ri-rss-line',
      color: 'orange',
    },
    {
      name: 'GitHub',
      link: 'https://github.com/krau',
      icon: 'i-ri-github-line',
      color: '#6e5494',
    },
    {
      name: '网易云音乐',
      link: 'https://music.163.com/#/user/home?id=1428613796',
      icon: 'i-ri-netease-cloud-music-line',
      color: '#C20C0C',
    },
    {
      name: '哔哩哔哩',
      link: 'https://space.bilibili.com/296036767',
      icon: 'i-ri-bilibili-line',
      color: '#FF8EB3',
    },
    {
      name: 'Telegram Channel',
      link: 'https://t.me/acherkrau',
      icon: 'i-ri-telegram-line',
      color: '#0088CC',
    },
    {
      name: 'Telegram Group',
      link: 'https://t.me/blogszh',
      icon: 'i-ri:telegram-fill',
      color: '&quot;#0088CC'
    },
    {
      name: 'E-Mail',
      link: 'mailto:me@krau.top',
      icon: 'i-ri-mail-line',
      color: '#8E71C1',
    },
    {
      name: 'Travelling',
      link: 'https://www.travellings.cn/go.html',
      icon: 'i-ri-train-line',
      color: 'var(--va-c-text)',
    },
  ],
</code></pre>
<h3>搜索</h3>
<ul>
<li>search
<ul>
<li>enable: 开启搜索功能:true</li>
<li>type: '搜索方式:local|engine|algolia'</li>
</ul>
</li>
</ul>
<p>示例:</p>
<pre><code class="language-ts">  search: {
    enable: true,
    type: 'engine',
  },
</code></pre>
<h3>赞助</h3>
<ul>
<li>sponsor
<ul>
<li>enable: true|false</li>
<li>title: '赞助标题'</li>
<li>methods
<ul>
<li>name: '赞助方式名称'</li>
<li>url: '链接'</li>
<li>color: '颜色'</li>
<li>icon: '图标'</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>示例:</p>
<pre><code class="language-ts">  sponsor: {
    enable: false,
    title: '我很可爱，请给我钱！',
    methods: [
      {
        name: '支付宝',
        url: '',
        color: '#00A3EE',
        icon: 'i-ri-alipay-line',
      },
      {
        name: 'QQ 支付',
        url: '',
        color: '#12B7F5',
        icon: 'i-ri-qq-line',
      },
      {
        name: '微信支付',
        url: '',
        color: '#2DC100',
        icon: 'i-ri-wechat-pay-line',
      },
    ],
  },
</code></pre>
<h3>yun主题配置</h3>
<p>valaxy 目前默认主题是 <a href="https://github.com/YunYouJun/valaxy/tree/main/packages/valaxy-theme-yun">yun</a></p>
<ul>
<li>theme: '主题名称'</li>
<li>themeConfig
<ul>
<li>banner
<ul>
<li>enable: 是否开启首页标题动画:true|false</li>
<li>title: '标题'</li>
</ul>
</li>
<li>bg_image:
<ul>
<li>enable: 是否开启背景图:true|false</li>
<li>url: '背景图链接'</li>
<li>dark: '深色模式下的背景图'</li>
<li>opactity: 背景不透明度:float</li>
</ul>
</li>
<li>colors:
<ul>
<li>primary: '主题色,支持16进制任意配色'</li>
</ul>
</li>
<li>say:
<ul>
<li>enable: 是否开启一言:true|false</li>
<li>api: '一言api地址,可留空使用默认'</li>
</ul>
</li>
<li>pages //页面配置
<ul>
<li>name: '名称'</li>
<li>url: '路径|链接'</li>
<li>icon: '图标'</li>
<li>color: '颜色'</li>
</ul>
</li>
<li>footer
<ul>
<li>since: 网站开始年份</li>
<li>beian
<ul>
<li>enable: 是否开启备案信息显示:true|false</li>
<li>icp: '备案信息'</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>示例:</p>
<pre><code class="language-ts">  theme: 'yun',

  themeConfig: {
    banner: {
      enable: true,
      title: '柯罗の自留地',
    },
    bg_image: {
      enable: true,
      url: '/photo/bg/atri.webp',
      dark: '/photo/bg/bg.webp',
      opacity: 0.7
    },
    colors: {
      primary: '#39c5bb'
    },
    say:{
      enable: false
    },
    pages: [
      {
        name: '友人帐',
        url: '/links/',
        icon: 'i-ri-genderless-line',
        color: 'dodgerblue',
      },
      {
        name: 'My Love',
        url: '/girls/',
        icon: 'i-ri-women-line',
        color: 'hotpink',
      },
    ],

    footer: {
      since: 2020,
      beian: {
        enable: false,
        icp: '苏ICP备17038157号',
      },

    },
  },
</code></pre>
<h3>评论</h3>
<p>valaxy 通过插件的方式实现评论，目前实现了对 <a href="https://waline.js.org">waline</a> 的支持</p>
<pre><code class="language-ts">  comment: {
    enable: true,
  },

  addons: [
    addonWaline({
      serverURL: '后端链接',
      //以下功能未实现，但可以先写进配置里
      pageview: true,
      dark: 'auto',
      requiredMeta: ['nick','mail'],
      locale:{
        placeholder: '填写邮箱，可以收到回复通知~'
      }
        
    }),
  ],
</code></pre>
<h3>完整配置文件示例</h3>
<pre><code class="language-ts">import { defineConfig } from 'valaxy'
import type { ThemeConfig } from 'valaxy-theme-yun'
import { addonWaline } from 'valaxy-addon-waline'
//import { defineConfig } from 'vite'
// add icons what you will need

/**
 * User Config
 */

export default defineConfig&lt;ThemeConfig&gt;({
  url: 'https://krau.top',
  lang: 'zh-CN',
  title: '柯罗krau的博客 | krau\'s blog',
  subtitle: '',
  author: {
    name: '柯罗krau',
    avatar: '/photo/avatar/avatar.jpg',
    status:
    {
      emoji: '😐',
      message: '真不想上学'
  }},
  mode: 'auto',
  lastUpdated: true,
  favicon: '',
  description: '年轻，就要折腾',
  social: [
    {
      name: 'RSS',
      link: '/atom.xml',
      icon: 'i-ri-rss-line',
      color: 'orange',
    },
    {
      name: 'GitHub',
      link: 'https://github.com/krau',
      icon: 'i-ri-github-line',
      color: '#6e5494',
    },
    {
      name: '网易云音乐',
      link: 'https://music.163.com/#/user/home?id=1428613796',
      icon: 'i-ri-netease-cloud-music-line',
      color: '#C20C0C',
    },
    {
      name: '哔哩哔哩',
      link: 'https://space.bilibili.com/296036767',
      icon: 'i-ri-bilibili-line',
      color: '#FF8EB3',
    },
    {
      name: 'Telegram Channel',
      link: 'https://t.me/acherkrau',
      icon: 'i-ri-telegram-line',
      color: '#0088CC',
    },
    {
      name: 'Telegram Group',
      link: 'https://t.me/blogszh',
      icon: 'i-ri:telegram-fill',
      color: '&quot;#0088CC'
    },
    {
      name: 'E-Mail',
      link: 'mailto:me@krau.top',
      icon: 'i-ri-mail-line',
      color: '#8E71C1',
    },
    {
      name: 'Travelling',
      link: 'https://www.travellings.cn/go.html',
      icon: 'i-ri-train-line',
      color: 'var(--va-c-text)',
    },
  ],

  search: {
    enable: true,
    type: 'engine',
  },

  sponsor: {
    enable: false,
    title: '我很可爱，请给我钱！',
    methods: [
      {
        name: '支付宝',
        url: '',
        color: '#00A3EE',
        icon: 'i-ri-alipay-line',
      },
      {
        name: 'QQ 支付',
        url: '',
        color: '#12B7F5',
        icon: 'i-ri-qq-line',
      },
      {
        name: '微信支付',
        url: '',
        color: '#2DC100',
        icon: 'i-ri-wechat-pay-line',
      },
    ],
  },

  theme: 'yun',

  themeConfig: {
    banner: {
      enable: true,
      title: '柯罗の自留地',
    },
    bg_image: {
      enable: true,
      url: '/photo/bg/atri.webp',
      dark: '/photo/bg/bg.webp',
      opacity: 0.7
    },
    colors: {
      primary: '#39c5bb'
    },
    say:{
      enable: false
    },
    pages: [
      {
        name: '友人帐',
        url: '/links/',
        icon: 'i-ri-genderless-line',
        color: 'dodgerblue',
      },
      {
        name: 'My Love',
        url: '/girls/',
        icon: 'i-ri-women-line',
        color: 'hotpink',
      },
    ],

    footer: {
      since: 2020,
      beian: {
        enable: false,
        icp: '苏ICP备17038157号',
      },

    },
  },

  unocss: {
    safelist: [
      'i-ri-home-line',
    ],
  },

  comment: {
    enable: true,
  },

  addons: [
    addonWaline({
      serverURL: 'https://后端',
      pageview: true,
      dark: 'auto',
      requiredMeta: ['nick','mail'],
      locale:{
        placeholder: '填写邮箱，可以收到回复通知~'
      }
        
    }),
  ],
})
</code></pre>
<h2>迁移文章</h2>
<h3>批量修改 frontmatter</h3>
<p>由于原先在 hexo 的文章有太多 valaxy 不支持的 frontmatter 项，所以需要批量修改它们，以免引起奇怪的问题</p>
<p>由于文章数多，所以写个简单的脚本批量操作</p>
<p>需要先安装 <code>python-frontmatter</code></p>
<pre><code class="language-shell">pip3 install python-frontmatter
</code></pre>
<pre><code class="language-python">import frontmatter
import glob

path = 'posts' #你的文章路径，相对于该脚本
mds = glob.glob(f'./{path}/*.md')

def del_metedata(md: str, key: str | list):
    post = frontmatter.load(md)
    metedata = post.metadata
    try:
        if isinstance(key, list):
            for k in key:
                try:
                    del metedata[k]
                except:
                    continue
        elif isinstance(key, str):
            del metedata[key]
        else:
            raise TypeError('key 是 str 或 list')
        frontmatter.dump(post, md)
    except Exception as e:
        print(f'{e} 在 {md}')


def add_metedata(md: str, key: str, value: str):
    post = frontmatter.load(md)
    metedata = post.metadata
    try:
        metedata[key] = value
        frontmatter.dump(post, md)
    except Exception as e:
        print(f'{e} 在 {md}')

def main():
    del_keys = [
        #要删除的项，你可以根据需要修改
        'toc',
        'abbrlink',
        'mathjax',
        'author',
        'hide',
        'sitemap',
        'indexing',
        'top',
        'password',
        'summary',
        'comments',
        'reward',
        'license',
        'edit',
        'copyright',
        'sticky',
        'description'
    ]
    for md in mds:
        del_metedata(md,del_keys)
        #添加你需要的项,如添加 layout: post
        add_metedata(md, 'layout', 'post')


if __name__ == '__main__':
    main()
</code></pre>
<h3>修改文章内图片路径</h3>
<p>如果你全站都使用的图床，可以忽略这一步。而我为了方便管理，把图片放在了本地，所以需要修改</p>
<p>原先是在 hexo 的 source 目录下，需要使用相对路径引用。而 valaxy 可以<strong>直接引用</strong>静态文件夹(<code>public</code>)内的文件，直接使用 /xxxx 即可</p>
<p>例如，我原先的图片路径是</p>
<p><code>../photos/post-pics/1.webp</code></p>
<p>那么只需要<strong>将原先在 hexo 博客目录的 photos 文件夹移动到 valaxy 的 public 文件夹</strong>下，然后将路径修改为</p>
<p><code>/photos/post-pics/1.webp</code></p>
<p>这里的修改可以使用 VSCode 的全局关键字替换功能</p>
<h3>移动文章</h3>
<p>把文章移动到 <code>博客文件夹/pages/posts</code> 下即可</p>
<p>但是我在操作的时候发现，一次性塞进去太多文章会导致预览页面加载不出来，这可能是一个 bug (版本 0.12.2 )。当然构建是没问题的，所以如果你不需要本地预览的话，可以直接都丢进去。</p>
<h2>迁移友联页面</h2>
<p>valaxy 的 yun 主题的友联页面在 pages/links/index.md，在 frontmatter 里配置就可以了，参考 <a href="https://github.com/YunYouJun/valaxy/tree/main/packages/valaxy-theme-yun/docs/zh-CN">valaxy-theme-yun 文档</a></p>
<p>注意 frontmatter 的下面要写一个</p>
<pre><code class="language-html">&lt;YunLinks :links=&quot;frontmatter.links&quot; :random=&quot;frontmatter.random&quot; /&gt;
</code></pre>
<h2>迁移 <a href="/girls/">girls</a> 页面</h2>
<p>与 <a href="https://yun.yunyoujun.cn/guide/page.html#girls">hexo-theme-yun</a> 的配置相同，但是 frontmatter 下面要加上</p>
<pre><code class="language-html">&lt;YunGirls :girls=&quot;frontmatter.girls&quot; :random=&quot;frontmatter.random&quot; /&gt;
</code></pre>
<p><s>哪来的二次元</s></p>
<hr>
<p>这样就完成迁移了。如果有其他问题，或者需要更多信息，可以对比<a href="https://valaxy.site/migration/hexo#%E7%A4%BA%E4%BE%8B">云游君的博客仓库</a>中 hexo 和 valaxy 分支的差异。或者提出 <a href="https://github.com/YunYouJun/valaxy/issues/new">issue</a> 、提出<a href="https://github.com/YunYouJun/valaxy/discussions/new/choose">讨论</a> ，以及在我博客下评论留言。</p>
<br/><p>访问 <a href="https://krau.top/posts/hexo-migrate-to-valaxy/index" target="_blank">https://krau.top/posts/hexo-migrate-to-valaxy/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2023-01-16T09:56:55.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用Python写一个Telegram bot吧|Telegram bot教程]]></title>
        <id>https://krau.top/posts/tg-bot-dev-note-kmua</id>
        <link href="https://krau.top/posts/tg-bot-dev-note-kmua"/>
        <updated>2022-12-20T10:22:19.000Z</updated>
        <content type="html"><![CDATA[<p>::: tip<br>
2024-01-14 更新:</p>
<p>修复了一些错误, 删除了一些不必要的内容</p>
<p><a href="https://t.me/acherkrau">讨论群</a></p>
<p>本文只是我最初写 bot 时的一点笔记, 更建议你直接看 <a href="https://docs.python-telegram-bot.org">Python-Telegram-Bot</a> 的文档<br>
:::</p>
<h2>前言</h2>
<p><s>为了激发群友们水群的积极性，</s> 为了学习和应用 python 知识，我最近在开发一个 telegram 的 bot，所以写这篇文章以记录和分享</p>
<p>在这篇文章中，将使用 <a href="https://docs.python-telegram-bot.org">Python-Telegram-Bot</a> ，基于 Python 的异步特性与 Telegram 友好开放的 API，开发一个兼顾实用性和趣味的 bot ，并使用 Docker 在任何地方部署 bot</p>
<p>Demo: <a href="https://github.com/krau/kmua-bot">kmua-bot</a></p>
<p>本文不是从零开始的教程，阅读本文前，你需要具有一点点(真的很少一点)的 python 编程的基础。</p>
<!-- more -->
<h2>准备</h2>
<h3>环境搭建</h3>
<p>使用你喜欢的工具创建虚拟环境，并安装 <code>python-telegram-bot</code> 库</p>
<pre><code class="language-shell">pip install python-telegram-bot
</code></pre>
<h3>bot 申请</h3>
<p>私聊 <a href="https://t.me/BotFather">@BotFather</a>。发送 <code>/newbot</code>，根据提示一步步创建，记得妥善保存最后的 <strong>API Token</strong>。</p>
<h3>获取你的 id</h3>
<p>每个 tg 用户都有一串唯一标识，即为 <strong>user_id</strong>，可以私聊 <a href="https://t.me/kmuav2bot">@kmua</a> 发送 <code>/id</code> 来获取它</p>
<h2>编程</h2>
<h3>开始:响应/start</h3>
<p>在项目文件夹内，新建 <code>bot.py</code>，开始编写 bot 最基础的功能，让其响应 /start 命令</p>
<p>首先，导入包</p>
<pre><code class="language-python">from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler
</code></pre>
<p>Update 类是从 Telegram 服务器获取到的各种&quot;更新&quot;, 包括而不限于用户发送的消息, 群组的信息变化等.</p>
<p>然后，写一个异步函数 start()，当收到 /start 命令时，要调用它。</p>
<pre><code class="language-python">async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    text = '你好~我是一个bot'
    await context.bot.send_message(chat_id=update.effective_chat.id,text=text)
</code></pre>
<p>这个函数接受两个参数 <code>update</code> 和 <code>context</code>，形参冒号后是类型注解。</p>
<p><code>context.bot.send_message()</code> 方法即为让 bot 发送消息，它能接受的参数其实很多，但是往往只需要 <code>chat_id</code> ，和 <code>text</code> 就够了，它们分别表示 要发送消息给的用户或群组 id ，要发送的文本。</p>
<p>而 <code>update.effective_chat.id</code> 即为当前有效对话的 id ，bot 收到的是哪里的消息 ，它就指向哪里的 id。</p>
<p>现在，实例化一个处理器(handler)</p>
<pre><code class="language-python">start_handler = CommandHandler('start', start)
</code></pre>
<p><code>CommandHandler</code> 类可以实现当收到某个命令时，调用某个函数（命令和函数名可以不一样），我们将其实例化为了 <code>start_handler</code> ，并且将命令名字 'start' 和对应要回调的函数名 <code>start</code> 传递给它</p>
<p>然后，启动bot</p>
<pre><code class="language-python"># 构建 bot
TOKEN='你 bot 的 api token'
application = ApplicationBuilder().token(TOKEN).build()
# 注册 handler
application.add_handler(start_handler)
# run!
application.run_polling()
</code></pre>
<p>最后，就可以使用 <code>python bot.py</code> 启动你的 bot ，对 bot 发送 /start ，它应该就会回复你 ”你好~我是一个bot”。<br>
使用 Ctrl + C 结束程序运行。</p>
<p>完整 <a href="http://bot.py">bot.py</a> :</p>
<pre><code class="language-python">from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    text = '你好~我是一个bot'
    await context.bot.send_message(chat_id=update.effective_chat.id,text=text)

start_handler = CommandHandler('start', start)

TOKEN='你 bot 的 api token'
application = ApplicationBuilder().token(TOKEN).build()
application.add_handler(start_handler)
application.run_polling()
</code></pre>
<p>::: tip<br>
如果你的环境无法直接访问 Telegram 服务器，可以设置代理</p>
<p>全局代理, 代理整个程序.</p>
<pre><code class="language-python">os.environ['http_proxy'] = '代理地址'
os.environ['https_proxy'] = '代理地址'
</code></pre>
<p>若想仅为 telegram bot api 设置代理,则需要改动一下构建 bot 部分的代码:</p>
<p>http(s) 代理:</p>
<pre><code class="language-python">proxy_url = 'http://USERNAME:PASSWORD@PROXY_HOST:PROXY_PORT'
application = ApplicationBuilder().token(&quot;TOKEN&quot;).proxy_url(proxy_url).get_updates_proxy_url(proxy_url).build()
</code></pre>
<p>socks5 代理,需要安装 <code>python-telegram-bot[socks]</code>, 然后:</p>
<pre><code class="language-python">proxy_url = &quot;socks5://user:pass@host:port&quot;
application = ApplicationBuilder().token(&quot;TOKEN&quot;).proxy_url(proxy_url).get_updates_proxy_url(proxy_url).build()
</code></pre>
<p>:::</p>
<p>到这里，其实你已经了解到了最基本的 telegram bot 编写规则，即：</p>
<ol>
<li>编写回调, 上面的例子中即为 <code>start</code> 这个函数.</li>
<li>决定调用回调函数的规则. 上述例子中，即为收到 <code>/start</code> 命令时, 调用 <code>start</code> 函数</li>
<li>实例化 <code>handler</code> , 注册给 <code>application</code>。上述例子中，即为 <code>application.add_handler(start_handler)</code></li>
</ol>
<p>接下来，写一些更有趣的功能，使用更复杂一些的规则来调用这些功能。</p>
<h3>授予群成员头衔</h3>
<p>让群成员可以通过 bot 自助获得一个头衔吧，比如，群友可以在群里使用 <code>/t@botname 好人</code> 来给自己加上 “好人” 的头衔。</p>
<p>当然，前提是 bot 自己要是管理员，并且具有相应的权限。</p>
<p>要实现这个功能，可以这样写:</p>
<pre><code class="language-python">async def set_right(update: Update, context: ContextTypes.DEFAULT_TYPE):
    chat_id = update.effective_chat.id
    user_id = update.effective_user.id
    bot_username_len = len(update._bot.name)
    custom_title = update.effective_message.text[3+bot_username_len:]
    if not custom_title:
        custom_title = update.effective_user.username
    try:
        await context.bot.promote_chat_member(chat_id=chat_id, user_id=user_id, can_manage_chat=True)
        await context.bot.set_chat_administrator_custom_title(chat_id=chat_id, user_id=user_id, custom_title=custom_title)
        text = f'好,你现在是{custom_title}啦'
        await context.bot.send_message(chat_id=chat_id, reply_to_message_id=update.effective_message.id, text=text)
    except:
        await context.bot.send_message(chat_id=chat_id, text='不行!!')
</code></pre>
<p>然后，添加这个功能的handler:</p>
<pre><code class="language-python">set_right_handler = CommandHandler('t', set_right)
application.add_handler(set_right_handler)
</code></pre>
<p>注意handler的添加应该在 <code>application.run_polling()</code> 之前，也就是说 <code>application.run_polling()</code> 才是真正开始运行 bot</p>
<p>简单解释一下 <code>set_right</code> 这个函数:</p>
<p>首先，设置了调用这个函数时的 <code>chat_id</code> 和 <code>user_id</code> 为当前聊天和当前用户</p>
<p>然后用 <code>len(update._bot.name)</code> 获取 bot 自己用户名的长度，用于下面的切片选取用户想要的头衔</p>
<p>而 <code>custom_title = update.effective_message.text[3+bot_username_len:]</code> 即获取用户想要的头衔，存储在 <code>custom_title</code> 中，下面的 if not 的作用是，如果用户没有发送他想要的头衔，那么默认头衔为他的用户名</p>
<p>由于只有管理员才有头衔，所以我们使用 promote_chat_member() 方法设置成员权限，将其的 can_manage_chat 权限设置为 True. 即使这样设置了，用户也只有一个管理员的名头，实际上没有任何权限</p>
<p>::: tip<br>
要想赋予权限，你可以向 promote_chat_member() 方法中继续添加参数，如 can_pin_messages=True 可置顶消息<br>
:::</p>
<p>然后就可以使用 <code>set_chat_administrator_custom_title()</code> 方法设置成员头衔了，如果成功了，会继续执行接下来的语句，将结果反馈给用户。</p>
<h3>响应未知命令</h3>
<p>如果 bot 只能回应设置好的命令就太无趣了，所以再编写一个当收到未知命令时的执行功能<br>
比如，想要让 bot 回应未知命令说 “我不会这个哦~”，可以这样写</p>
<pre><code class="language-python">async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await context.bot.send_message(chat_id=update.effective_chat.id, text=&quot;我不会这个哦~&quot;)
</code></pre>
<p>然后，添加它的 <code>handler</code>。这时候注意到：</p>
<p>前面的两个功能都是回应一个特定的命令，在这里，我们希望它回应除已经设定的命令外的其他所有命令，该怎么做？</p>
<p>这就需要用到 <code>filters</code> ，即过滤需要处理器处理的更新. 并且要<strong>设置功能的优先级</strong></p>
<p>首先导入它</p>
<pre><code class="language-python">from telegram.ext import filters
</code></pre>
<p>然后可以这样写:</p>
<pre><code class="language-python">unknown_handler = MessageHandler(filters.COMMAND, unknown)
</code></pre>
<p><code>filters.COMMAND</code> 即为过滤出命令。而设置优先级其实很简单，在不对处理器分组的情况下, <strong>添加 <code>handler</code> 的顺序越靠后，对应功能的优先级越低</strong></p>
<p>也就是说，我们需要在其他命令类 <code>handler</code> 之后，添加 <code>unknown_handler</code></p>
<pre><code class="language-python">application.add_handler(start_handler)
application.add_handler(set_right_handler)
application.add_handler(unkonw_handler)

application.run_polling()
</code></pre>
<h3>简单的关键词回复</h3>
<p>bot 最常见的功能之一就是根据关键词回复特定内容，比如，当对 bot 说早安之类的话时，让 bot 对此做出回应。下面来实现这这一功能</p>
<p>由于命令和普通消息是两种不同的消息类，处理它们的方法是不一样的，针对非命令消息，要使用 <code>MessageHandler</code>:</p>
<pre><code class="language-python">from telegram.ext import MessageHandler
</code></pre>
<p>首先还是要编写这个功能执行的函数。</p>
<pre><code class="language-python">import random

async def ohayo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    texts = ['早上好呀','我的小鱼你醒了，还记得清晨吗','哦哈哟~']
    await context.bot.send_message(chat_id=update.effective_chat.id, text=random.choice(text))
</code></pre>
<p>由于只回复一个固定的消息过于单调，所以我们使用 <code>random</code> 来从一个列表中随机选择一个回复</p>
<p>仍然是前文所提到的, 回调函数只负责发送消息，而何时调用这个函数，并不是这个函数本身要做的事，而是由接下来我们要编写的 <code>MessageHandler</code> 中的 <code>filters</code> 决定的:</p>
<pre><code class="language-python">filter_ohayo = filters.Regex('早安|早上好|哦哈哟|ohayo')
ohayo_handler = MessageHandler(filter_ohayo, ohayo)
</code></pre>
<p><code>filters.Regex()</code> 即为使用正则表达式过滤，我们使用 <code>'早安|早上好|哦哈哟|ohayo'</code> 这个表达式，很容易理解何时会调用 <code>ohayo</code> 这个函数</p>
<p>记得添加它的 <code>handler</code></p>
<pre><code class="language-python">application.add_handler(ohayo_handler)
</code></pre>
<hr>
<p>所有完整 <a href="http://bot.py">bot.py</a>:</p>
<pre><code class="language-python">import random

from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, filters, MessageHandler

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    text = '你好~我是一个bot'
    await context.bot.send_message(chat_id=update.effective_chat.id,text=text)


async def set_right(update: Update, context: ContextTypes.DEFAULT_TYPE):
    chat_id = update.effective_chat.id
    user_id = update.effective_user.id
    bot_username_len = len(update._bot.name)
    custom_title = update.effective_message.text[3+bot_username_len:]
    if not custom_title:
        custom_title = update.effective_user.username
    try:
        await context.bot.promote_chat_member(chat_id=chat_id, user_id=user_id, can_manage_chat=True)
        await context.bot.set_chat_administrator_custom_title(chat_id=chat_id, user_id=user_id, custom_title=custom_title)
        text = f'好,你现在是{custom_title}啦'
        await context.bot.send_message(chat_id=chat_id, reply_to_message_id=update.effective_message.id, text=text)
    except:
        await context.bot.send_message(chat_id=chat_id, text='不行!!')


async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await context.bot.send_message(chat_id=update.effective_chat.id, text=&quot;我不会这个哦~&quot;)


async def ohayo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    texts = ['早上好呀','我的小鱼你醒了，还记得清晨吗','哦哈哟~']
    await context.bot.send_message(chat_id=update.effective_chat.id, text=random.choice(text))


start_handler = CommandHandler('start', start)
set_right_handler = CommandHandler('p', set_right)
unknown_handler = MessageHandler(filters.COMMAND, unknown)
filter_ohayo = filters.Regex('早安|早上好|哦哈哟|ohayo')
ohayo_handler = MessageHandler(filter_ohayo, ohayo)

TOKEN='你 bot 的 api token'
application = ApplicationBuilder().token(TOKEN).build()
application.add_handler(start_handler)
application.add_handler(set_right_handler)
application.add_handler(unkonw_handler)
application.add_handler(ohayo_handler)
# run!
application.run_polling()
</code></pre>
<h2>部署</h2>
<p>python 程序的环境管理很烦人,用 docker 来跑再合适不过.</p>
<h3>Docker 本地构建</h3>
<p>如果要在本地构建 docker 镜像,参考下面的 <code>Dockerfile</code> 文件</p>
<pre><code class="language-Dockerfile">FROM python:3.12-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
ENTRYPOINT [ &quot;python&quot;,&quot;/app/bot.py&quot; ]
</code></pre>
<p>将 Dockerfile 文件放到合适的目录(项目根目录即可),然后执行</p>
<p><code>docker build -t bot .</code></p>
<p>注意后面有一个 .</p>
<p>然后使用 <code>docker run -d bot</code> 启动容器,运行 bot</p>
<h3>使用 GitHub action 自动构建</h3>
<p>github action 可以构建 docker 镜像并发布到 ghcr,方便在其他地方部署 docker 容器.你依然需要在项目中写好 <code>Dockerfile</code></p>
<p>在项目中新建 <code>.github/workflows/build-docker.yml</code>,参考以下配置</p>
<pre><code class="language-yml">name: Build and publish docker container

on:
 push:
     branches:
     - main
  workflow_dispatch:
    
    

jobs:
  publish:
    name: Publish container image
    runs-on: ubuntu-20.04
    env:
      TZ: Asia/Shanghai
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: OCI meta
        id: meta
        uses: docker/metadata-action@v4.1.1
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=edge,branch=main
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
        
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Login to GHCR
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v3
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          platforms: linux/amd64,linux/arm64
          cache-from: type=gha
          cache-to: type=gha,mode=max
</code></pre>
<p>然后可以使用 <code>docker pull</code> 的方法运行,或者使用 docker compose:</p>
<p>新建 <code>docker-compose.yml</code>,参考以下内容</p>
<pre><code class="language-yml">version: &quot;3&quot;
services:
  kmua:
    image: ghcr.io/krau/kmua-bot:main
    container_name: kmua-main
    init: true
    volumes:
      - ./data:/kmua/data
      - ./logs:/kmua/logs
      - ./config.yml:/kmua/config.yml
    environment:
      - TZ=Asia/Shanghai
</code></pre>
<p>注意根据自己需要更改</p>
<p>然后就可以使用 <code>docker compose up -d</code> 启动容器</p>
<br/><p>访问 <a href="https://krau.top/posts/tg-bot-dev-note-kmua" target="_blank">https://krau.top/posts/tg-bot-dev-note-kmua</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-12-20T10:22:19.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[ChatGPT简单使用体验|你的下一个Google，何必是搜索引擎]]></title>
        <id>https://krau.top/posts/ChatGPT-simple-exp/index</id>
        <link href="https://krau.top/posts/ChatGPT-simple-exp/index"/>
        <updated>2022-12-05T20:47:50.000Z</updated>
        <content type="html"><![CDATA[<p>最近 openai 的 <a href="https://chat.openai.com/chat">ChatGPT</a> 有点小火，看了官方和民间的一些截图之后，感觉这东西确实有点意思，所以就想办法注册了个号来体验体验。</p>
<!-- more -->
<p>结论：ChatGPT像一面镜子，只要问题详尽，描述清楚，那么它就能给你一个基本准确的答案，这比使用谷歌搜索要节省很多时间。但是有些问题它给出的答案还有待优化。</p>
<p>下面是我提问的一些问题。</p>
<p>Linux使用相关</p>
<p>切换个时区，完全正确，简简单单</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-20-57-25.n37jtTFT.webp" alt=""></p>
<p>装个docker</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-20-59-13.Bxb0kO4i.webp" alt=""></p>
<p>nginx的使用，示例配置文件写的格式也对</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-01-20.DMJXjYb8.webp" alt=""></p>
<p><img src="https://krau.top/posts/ChatGPT-simple-exp/ChatGPT-simple-exp_2022-12-05-21-03-33.webp" alt=""></p>
<p>这个我感觉最逆天，它能针对一个具体的项目给出使用文档</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-05-57.CyO3gNfm.webp" alt=""></p>
<p>Python编程相关</p>
<p>字符串匹配问题，<s>我以为它会给我写一个ac算法</s>，没想到直接用的for in，只能说又不是不能用吧</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-15-11.BfOck9qY.webp" alt=""></p>
<p>简单的json使用问题</p>
<p><img src="https://krau.top/posts/ChatGPT-simple-exp/ChatGPT-simple-exp_2022-12-05-21-16-28.webp" alt=""></p>
<p>python的日期和时间</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-17-41.DRuQU3l0.webp" alt=""></p>
<p>来排个经典的解码问题错误吧</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-19-37.JgFtCpqF.webp" alt=""></p>
<p>还要第三方包的导入问题</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-24-13.MopyhgbB.webp" alt=""></p>
<p>这个就有点厉害了，给出的答案基本是正确的，可以直接使用。如果使用Google解决这些问题，那么要花费一定的时间去翻看不同网站的回答，还不一定准确。</p>
<p>其他</p>
<p>来问个我以前写过的文章（</p>
<p><img src="https://krau.top/posts/ChatGPT-simple-exp/ChatGPT-simple-exp_2022-12-05-21-30-19.webp" alt=""></p>
<p>有点厉害，但是不是我想要的结果，我是想把图片直接保存在本地的，所以我又追问它：</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-33-17.DWLlHzmg.webp" alt=""></p>
<p>结果给了我一个最麻烦的办法，所以我继续追问：</p>
<p><img src="https://krau.top/assets/ChatGPT-simple-exp_2022-12-05-21-35-23.CexeHhG1.webp" alt=""></p>
<p>似乎我想要的答案要出来了，但是它突然卡壳了，不知道怎么回事😅</p>
<p>总结</p>
<p>就这样，总体感觉是，在某些场景它比Google是要好用很多的，但是要说取代谷歌，我觉得还有点距离。在简单而足够具体的问题上，使用chatGPT要比使用Google效率高的多，但是比较抽象的问题就不是了。</p>
<br/><p>访问 <a href="https://krau.top/posts/ChatGPT-simple-exp/index" target="_blank">https://krau.top/posts/ChatGPT-simple-exp/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-12-05T20:47:50.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[如何加入一个zerotier网络]]></title>
        <id>https://krau.top/posts/zerotier-client/index</id>
        <link href="https://krau.top/posts/zerotier-client/index"/>
        <updated>2022-08-21T09:45:30.000Z</updated>
        <content type="html"><![CDATA[<p>最近把自家宽带从联通转战移动，然而移动不给公网ipv4，导致我mc服的铸币玩家们无法连接服务器，于是决定用zerotier 组建网络，一举多得.</p>
<p>此文只介绍如何加入一个zerotier网络。</p>
<!-- more -->
<p>打开<a href="https://www.zerotier.com/download/">官网的下载页面</a></p>
<p>点击windows即可下载</p>
<p><img src="https://krau.top/posts/zerotier-client/zerotier-client_2022-08-21-09-59-02.webp" alt="4"></p>
<p>下载完后双击运行，自动完成安装</p>
<p>安装完成后，点击状态栏图标，点击join new network</p>
<p><img src="https://krau.top/assets/zerotier-client_2022-08-21-10-01-09.Dnih9Noq.webp" alt="5"></p>
<p>然后填入要加入的网络id，点击join即可</p>
<br/><p>访问 <a href="https://krau.top/posts/zerotier-client/index" target="_blank">https://krau.top/posts/zerotier-client/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-08-21T09:45:30.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[在vscode里直接粘贴图片——pasteimage]]></title>
        <id>https://krau.top/posts/pasteimage-in-vs/index</id>
        <link href="https://krau.top/posts/pasteimage-in-vs/index"/>
        <updated>2022-07-07T09:16:30.000Z</updated>
        <content type="html"><![CDATA[<h2>前言</h2>
<p>使用vscode写博客，要想便捷快速地在文章中插入图片，比较常见的一个做法是通过picgo+图床实现快捷键上传图片，并生成markdown格式的链接。图床常常是利用jsdelivr加速github仓库的方式搭建。</p>
<p>我原本也一直是这样做的，但自从jsdelivr在国内不可用之后，（再者，jsdelivr的使用规范也不允许作为图床使用）我换用了另一种方案，即通过vscode的pasteimage插件直接把图片粘贴在文章内，并保存在博客目录内。</p>
<!-- more -->
<h2>效果展示</h2>
<p>为了突出这个插件的好用，特把效果展示提到使用教程前。</p>
<p>首先，截张图，或者复制一张图片</p>
<p><img src="https://krau.top/assets/pasteimage-in-vs_2022-07-07-09-31-48.CeOQWIqc.webp" alt=""></p>
<p>然后在vscode内，使用快捷键ctrl+alt+v（可自定义）粘贴这张图片</p>
<p><img src="https://krau.top/assets/pasteimage-in-vs_2022-07-07-09-32-56.DqjgABkc.webp" alt=""></p>
<p>这样就完成了，粘贴到文章内的图片会保存在你设置的目录下，粘贴图片实际上是粘贴的图片路径。图片文件以{文章标题_时间}的格式命名，十分有条理。</p>
<p>vscode内的实际效果↓↓↓</p>
<p><img src="https://krau.top/assets/pasteimage-in-vs_2022-07-07-09-35-12.RuJQzI4p.webp" alt=""></p>
<p>图片文件名↓↓↓</p>
<p><img src="https://krau.top/assets/pasteimage-in-vs_2022-07-07-09-37-18.ImtqXd02.webp" alt=""></p>
<h2>使用</h2>
<p>假设你的博客的根目录文件夹名是<code>hexo-blog-raw</code>，你想把粘贴的图片以<code>文章标题_时间.png</code>的格式命名，并保存在<code>hexo-blog-raw/source/photo</code>目录下</p>
<h3>教程</h3>
<p>首先，在vscode的扩展商店中，搜索 paste image并安装</p>
<p><img src="https://krau.top/assets/pasteimage-in-vs_2022-07-07-09-38-09.D7cfjTgG.webp" alt=""></p>
<p>然后，在博客根目录下，新建.vscode文件夹，用于存放vscode的工作区配置（即局部设置）</p>
<p>在.vscode文件夹内，新建settings.json文件，并写入以下内容并保存。</p>
<pre><code class="language-json">{
    &quot;pasteImage.namePrefix&quot;: &quot;${currentFileNameWithoutExt}_&quot;,
    &quot;pasteImage.path&quot;: &quot;${projectRoot}/source/photo&quot;,
    &quot;pasteImage.basePath&quot;: &quot;${projectRoot}/source&quot;,
    &quot;pasteImage.forceUnixStyleSeparator&quot;: true,
    &quot;pasteImage.prefix&quot;: &quot;../&quot;
}
</code></pre>
<p><img src="https://krau.top/posts/pasteimage-in-vs/pasteimage-in-vs_2022-07-07-09-48-46.webp" alt=""></p>
<p>这样就完成了。</p>
<p>按照如上配置后，所有配置是只在hexo-blog-raw工作区生效的，即只有在用vscode写博客文章时，以上配置才生效。</p>
<p>使用时，用vscode打开博客文章，用快捷键ctrl+alt+v即可插入图片到文章内。</p>
<p>更多的自定义配置，可参考<a href="https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image">pasteimage官方文档</a></p>
<br/><p>访问 <a href="https://krau.top/posts/pasteimage-in-vs/index" target="_blank">https://krau.top/posts/pasteimage-in-vs/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-07-07T09:16:30.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[自己动手制作网线水晶头]]></title>
        <id>https://krau.top/posts/make-cable/index</id>
        <link href="https://krau.top/posts/make-cable/index"/>
        <updated>2022-06-20T22:26:35.000Z</updated>
        <content type="html"><![CDATA[<p>我最近买了一台二手主机当nas用, 但是由于我家的布局设置, 必须要把这台主机放置在离路由器较远的一个位置。所以我直接买了根超长网线 (指30m) 以防长度不够, 但是到手之后发现, 我严重高估了我所需的长度, 于是把这根长网线剪去一大段, 并去学会了接网线水晶头的操作。</p>
<!-- more -->
<h2>准备工具</h2>
<p>制作水晶头需要以下工具：</p>
<ol>
<li>网线钳（某宝十块钱左右一个, 还送一些水晶头和墙钉）</li>
<li>水晶头（买网线钳送）</li>
<li>剥线刀（买网线钳也送）</li>
</ol>
<p>所以实际上你只需要去某宝购买一个小套装即可。</p>
<h2>实操</h2>
<p>一般购买到的一根超长网线, 两端是接好水晶头的, 但是我们不需要这么长, 所以截取你需要的长度, 用剪刀剪下一端。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-37-16.CVkwHcNT.webp" alt=""></p>
<p>然后用剥线刀把这端的一段外皮剥去。把剥线刀在剥线位点上转一圈即可。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-38-39.0vVM4dh1.webp" alt=""></p>
<p>剥完皮是这样的</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-39-25.CGsow4rd.webp" alt=""></p>
<p>仔细观察, 这根网线内有八根线, 各自有着不同的颜色, 尤其注意白线, 它们带有的颜色也是不同的。这八根线色分别是 <strong>橙白、绿白、蓝白、棕白、橙色、绿色、蓝色、棕色</strong>。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-43-11.CIH20qm8.webp" alt=""></p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-43-20.BspcJDr2.webp" alt=""></p>
<p>接下来把这八根线理顺好, 用网线钳把它们剪成齐平。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-44-37.BfzCKeSz.webp" alt=""></p>
<p>然后理顺八根线的次序, 如图（引用自淘宝某店铺）。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-46-03.B7Bnpkqf.webp" alt=""></p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-46-23.DT2SCIiw.webp" alt=""></p>
<p>再把线塞入水晶头, 注意排列顺序与水晶头的正反。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-47-27.B93S55Bs.webp" alt=""></p>
<p>最后用网线钳压下水晶头, 听见咔哒声就算完成了。</p>
<p><img src="https://krau.top/assets/make-cable_2022-06-20-22-49-06.DH6wpsC7.webp" alt=""></p>
<br/><p>访问 <a href="https://krau.top/posts/make-cable/index" target="_blank">https://krau.top/posts/make-cable/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-06-20T22:26:35.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Minecraft开服入门]]></title>
        <id>https://krau.top/posts/mc-host-server/index</id>
        <link href="https://krau.top/posts/mc-host-server/index"/>
        <updated>2022-06-20T10:00:38.000Z</updated>
        <content type="html"><![CDATA[<p>想和基友们联机玩mc，相比于使用各种联机平台，或搭建虚拟局域网，不如自己直接开个服务器。既不用保持至少一人在线，又能避免玩游戏时需要额外运行一个联机程序的臃肿。</p>
<p>本文记录博主自己的mc开服经历。</p>
<!-- more -->
<h2>工具选择与准备</h2>
<h3>主机选择</h3>
<p>我起初有开mc服的计划时是还在上高三，没有详细了解各方面的信息，因此当时想拉上几个基友集资购买云主机，在云主机上开服。</p>
<p>但高考完放假之后我去了解了一下云主机的价格，还是把原来的打算作废了。</p>
<p>我的需求是让我的服务器 <strong>安装一些大型模组并保证十人能同时在线不卡顿</strong>。而显然价格相对低的轻量云主机（指几百块一年）难以满足。</p>
<p>于是我决定在自己电脑上开服。但是用自己的主力机实在感觉不妥，一来它的高功耗会导致电费的额外支出，<s>二来心疼它二十四小时不关机</s>。</p>
<p>最终，我决定再组一台低功耗主机，用来开设mc服务器的同时，还能当nas用。</p>
<p>但是我发现，由于洋垃圾市场炒的厉害，分别购买cpu、主板、内存、硬盘配件，还不如直接购买二手整机来的划算。于是去海鲜市场（闲鱼）买了台整机。</p>
<p><img src="https://krau.top/assets/mc-host-server_1.D8QXfsbU.webp" alt=""></p>
<p>以T结尾的intel处理器代表低功耗，正适合作为服务器使用。</p>
<p>到手之后重装了Windows10 LTSC。</p>
<h3>游戏版本选择</h3>
<p>游戏版本选择是我最纠结的地方，由于学业原因，我近乎三年没有碰mc，再回过来看的时候最新版本已经是1.19了，而且fabric似乎也流行了起来，越来越多的模组和资源包开始仅支持较高版本且仅兼容fabric，就连java的最新版本居然也到了17。但我更熟悉的是forge和旧版本。本想跟联机基友们共同商量，可他们也是什么都不知道。最终抱着延续初中的玩法的想法，选择了1.12.2这个时至今日已经是远古的版本。</p>
<h3>服务端软件选择</h3>
<p>由于我此前只有建设mc基岩版服的经验，当时基岩版只有官方服务端，所以也没有什么选择。</p>
<p>但je（java edition）服务端不一样，各种服务端看的我眼花缭乱，最终草率地随大流，选择了<a href="https://catmc.org/">Catserver（猫端）</a>这一使用人数众多的服务端。</p>
<p><s>先开了再说，大不了redo</s></p>
<h2>开服</h2>
<p>选择好以上这些东西之后，就有了开服的方向，接下来的事情就很简单了，只需要动手去做就好了。</p>
<h3>配置服务端</h3>
<h4>下载与初始化catserver服务端</h4>
<p>先在java官网，下载java8。</p>
<p>前往<a href="https://catmc.org/">catserver官网</a>或<a href="https://github.com/Luohuayu/CatServer">github仓库</a>下载所需版本的catserver。</p>
<p>下载的是一个.jar单文件，把它重命名为Catserver.jar放入一个空文件夹，该文件夹名与路径尽量使用纯英文，避免出现奇怪的错误。</p>
<p>在该空文件夹内，新建一个文本文档，并写入以下内容：</p>
<pre><code class="language-bat">java -Xmx6G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -Dfml.queryResult=confirm -jar CatServer.jar
</code></pre>
<p>这段内容的解释：</p>
<p>-Xmx6G ：设定服务端最大内存为6G</p>
<p>-XX等参数：开启G1GC和优化GC，<a href="https://www.mcbbs.net/thread-1085422-1-1.html">详见此贴</a></p>
<p>然后把该文本重命名为strat.bat，双击运行该bat。</p>
<p>之后，猫端会自动下载所需的文件，并释放到同目录下，这样就完成了服务端的初始化。初始化完成后的目录文件如下</p>
<p><img src="https://krau.top/assets/mc-host-server_2.Bhh7q2Ma.webp" alt=""></p>
<h4>修改服务端配置文件</h4>
<p>初始化完成后，需要修改以下几个文件</p>
<ul>
<li>server.properties</li>
<li>bukkit.yml</li>
<li>catserver.yml</li>
<li>spigot.yml</li>
</ul>
<p>参考这两帖，根据需求进行修改。</p>
<p><a href="https://www.mcbbs.net/forum.php?mod=viewthread&amp;tid=774469"> [服务器应用] 【优化】服务器优化指南 | 和卡顿永远说再见 </a></p>
<p><a href="https://www.mcbbs.net/forum.php?mod=viewthread&amp;tid=478126"> [服务器应用] Minecraft服务器优化教程 —— 让多带50%的玩家不再是梦 </a></p>
<p>下面是我修改后的配置文件</p>
<ul>
<li>server.properties</li>
</ul>
<pre><code class="language-ini">#Minecraft server properties
#Sun Jun 19 19:24:15 CST 2022
spawn-protection=16
max-tick-time=60000
generator-settings=
force-gamemode=false
allow-nether=true
gamemode=0
broadcast-console-to-ops=true
enable-query=false
player-idle-timeout=0
difficulty=2
spawn-monsters=true
op-permission-level=4
pvp=true
snooper-enabled=true
level-type=DEFAULT
hardcore=false
enable-command-block=true
max-players=10
network-compression-threshold=-1
resource-pack-sha1=
max-world-size=29999984
server-port=25565
server-ip=0.0.0.0
spawn-npcs=true
allow-flight=true
level-name=world
view-distance=8
resource-pack=
spawn-animals=true
white-list=true #开启白名单
generate-structures=true
online-mode=flase #关闭正版验证
max-build-height=256
level-seed=
prevent-proxy-connections=false
enable-rcon=false
motd=fish world
</code></pre>
<ul>
<li>bukkit.yml</li>
</ul>
<pre><code class="language-yaml">settings:
  allow-end: true
  warn-on-overload: true
  permissions-file: permissions.yml
  update-folder: update
  plugin-profiling: false
  connection-throttle: 4000
  query-plugins: true
  deprecated-verbose: default
  shutdown-message: Server closed
spawn-limits:
  monsters: 50
  animals: 15
  water-animals: 5
  ambient: 3
chunk-gc:
  period-in-ticks: 400
  load-threshold: 300
ticks-per:
  animal-spawns: 400
  monster-spawns: 2
  autosave: 6000
aliases: now-in-commands.yml
</code></pre>
<ul>
<li>catserver.yml</li>
</ul>
<pre><code class="language-yaml">world:
  keepSpawnInMemory: false
  enableSkipEntityTick: true
  enableSkipTileEntityTick: false
  worldGenMaxTick: 15
  disableForgeGenerateWorlds:
  - ExampleCustomWorld
  preventBlockLoadChunk: false
  autoUnloadDimensions:
  - 99999999
  enableRealtime: true
  forceSaveOnWatchdog: true
  maxEntityCollision: 8
  saveBukkitWorldDimensionId: true
fakePlayer:
  permissions:
  - essentials.build
  eventPass: false
vanilla:
  fixPlayBossSoundToOtherWorld: true
  fixLessCrystalRespawnDragon: false
  preventPistonPushRail: false
  preventPistonPushSlimeBlock: false
  limitFastClickGUI: false
plugin:
  patcher:
    enableDynmapCompatible: true
    enableCoreProtectModBlockCompatible: true
    enableEssentialsNewVersionCompatible: true
    enableMythicMobsPatcherCompatible: true
    enableWorldEditCompatible: true
    enableCitizensCompatible: true
  disableHopperMoveEventWorlds: []
  defaultInstall:
    spark: true
async:
  waitForgeServerChatEvent: false
security:
  opManager: false
  gameModeManager: false
disableUpdateGameProfile: false
disableFMLHandshake: false
disableFMLStatusModInfo: false
disableAsyncCatchWarn: false
versionCheck: true
</code></pre>
<ul>
<li>spigot.yml</li>
</ul>
<pre><code class="language-yaml">config-version: 11
settings:
  debug: false
  attribute:
    maxHealth:
      max: 2048.0
    movementSpeed:
      max: 2048.0
    attackDamage:
      max: 2048.0
  netty-threads: 4
  timeout-time: 60
  restart-on-crash: true
  restart-script: ./start.sh
  moved-too-quickly-multiplier: 10.0
  moved-wrongly-threshold: 0.0625
  filter-creative-items: true
  bungeecord: false
  sample-count: 12
  late-bind: false
  int-cache-limit: 1024
  item-dirty-ticks: 20
  user-cache-size: 1000
  player-shuffle: 0
  save-user-cache-on-stop-only: false
messages:
  whitelist: You are not whitelisted on this server!
  unknown-command: Unknown command. Type &quot;/help&quot; for help.
  server-full: The server is full!
  outdated-client: Outdated client! Please use {0}
  outdated-server: Outdated server! I'm still on {0}
  restart: Server is restarting
stats:
  disable-saving: false
  forced-stats: {}
commands:
  log: true
  replace-commands:
  - setblock
  - summon
  - testforblock
  - tellraw
  spam-exclusions:
  - /skill
  silent-commandblock-console: false
  tab-complete: 0
advancements:
  disable-saving: false
  disabled:
  - minecraft:story/disabled
world-settings:
  default:
    verbose: true
    merge-radius:
      item: 2.5
      exp: 3.0
    arrow-despawn-rate: 1200
    wither-spawn-sound-radius: 0
    nerf-spawner-mobs: false
    entity-tracking-range:
      players: 48
      animals: 48
      monsters: 48
      misc: 32
      other: 64
    view-distance: 10
    enable-zombie-pigmen-portal-spawns: true
    zombie-aggressive-towards-villager: true
    item-despawn-rate: 6000
    hanging-tick-frequency: 100
    growth:
      cactus-modifier: 100
      cane-modifier: 100
      melon-modifier: 100
      mushroom-modifier: 100
      pumpkin-modifier: 100
      sapling-modifier: 100
      wheat-modifier: 100
      netherwart-modifier: 100
      vine-modifier: 100
      cocoa-modifier: 100
    entity-activation-range:
      animals: 32
      monsters: 28
      misc: 16
      tick-inactive-villagers: true
    random-light-updates: false
    ticks-per:
      hopper-transfer: 8
      hopper-check: 1
    hopper-amount: 1
    save-structure-info: true
    hunger:
      jump-walk-exhaustion: 0.05
      jump-sprint-exhaustion: 0.2
      combat-exhaustion: 0.1
      regen-exhaustion: 6.0
      swim-multiplier: 0.01
      sprint-multiplier: 0.1
      other-multiplier: 0.0
    max-tick-time:
      tile: 50
      entity: 50
    squid-spawn-range:
      min: 45.0
    max-tnt-per-tick: 100
    mob-spawn-range: 4
    dragon-death-sound-radius: 0
    seed-village: 10387312
    seed-feature: 14357617
    seed-monument: 10387313
    seed-slime: 987234911
</code></pre>
<h4>在服务端安装模组和插件</h4>
<p>在服务端中，有mods和plugins两个文件夹，它们分别是游戏模组和服务端插件。</p>
<p>游戏模组是增强和丰富游戏内容的，而服务端插件是增强服务端功能的。</p>
<p>需要安装的模组，都放在mods文件夹内。</p>
<p>在安装模组时，要看清模组是否需要在服务端安装，不需要在服务端安装的模组不要安装在服务端。（如waila高亮显示等模组）</p>
<p><img src="https://krau.top/posts/mc-host-server/mc-host-server_3.webp" alt="">_服务端需要装</p>
<p><img src="https://krau.top/assets/mc-host-server_4.EQVUNejh.webp" alt="">_不要在服务端装</p>
<p>另外在安装模组时，还要留心查看该模组的说明，有些模组需要额外的前置模组，要一起安装（如植物魔法botania需要bauble作为前置模组，二者必须都安装）<br>
以下是我在服务端安装的模组</p>
<p><img src="https://krau.top/assets/mc-host-server_5.DqXJOUas.webp" alt=""></p>
<p>而服务端插件直接放如plugins文件夹即可，可以根据需要去搜索。我曾经安装了<a href="https://github.com/DreamVoid/MiraiMC">miraimc</a>插件，它是一个能双向同步mc服务器和qq群消息的插件，但由于性能和兼容性问题又把它移除了。目前我只安装了一个luckperm插件，用来管理玩家权限（如单独给玩家tp权限而不给op管理员身份）</p>
<h4>设定op、添加白名单</h4>
<p>服务端启动完成后，直接在服务端控制台窗口可运行指令（包括游戏内的指令，如 time set）</p>
<p>执行<code>op &lt;玩家名&gt;</code>可赋予玩家op</p>
<p>执行<code>whitelist add &lt;玩家名&gt;</code>可为玩家添加白名单</p>
<p>注意在控制台窗口输入命令时，都不带<code>/</code></p>
<h3>配置客户端、制作整合包</h3>
<p>客户端即是玩家端，可以选择任意一款启动器，制作整合包发给发给玩家，而我选择的是HMCL这个老牌启动器，版本为3.5.3</p>
<p>下载游戏本体<br>
<img src="https://krau.top/assets/mc-host-server_6.Cmwvk9X4.webp" alt=""></p>
<p>然后把所安装的模组放入游戏目录的mods文件夹</p>
<p>可以使用<a href="https://littleskin.cn">litteskin</a>，为游戏增加自定义皮肤支持，同时还需要安装customskinloader 模组。</p>
<p>然后启动游戏，在多人游戏中添加搭建的服务器，退出即可。</p>
<p>之后在hmcl中，选择导出整合包，导出时，勾选多人服务列表一起导出。为了以后更新方便，我还构建了服务器自动更新整合包，在hmcl启动器中和mcbbs上均有相关教程。<br>
参考<br>
<a href="https://lo-li.cc/2021/03/11/Nginx%E4%B8%8B%E9%85%8D%E7%BD%AEHMCL%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%E6%95%B4%E5%90%88%E5%8C%85">Nginx 下配置 HMCL 自动更新整合包</a></p>
<p>由于构建步骤相对繁琐，我也会另写一篇文章记录。</p>
<p>把制作好的整合包发给玩家后，若使用的是云主机，就可以直接开始联机游戏了。但我用的是自己的主机，其他非内网玩家是无法访问我的服务器的，所以还需要进行端口映射和ddns配置（或内网穿透）</p>
<h3>使外部网络可访问服务器</h3>
<p>我的路由器支持花生壳的ddns，所以我采用了端口映射+ddns的方法。</p>
<p>在路由器管理页面，进行如下配置映射端口（默认端口为25565，可在服务端配置文件server.properties中更改）</p>
<p><img src="https://krau.top/assets/mc-host-server_7.DXJPJMxY.webp" alt=""></p>
<p>然后登陆花生壳ddns账号，这样外部网络便可以访问局域网中的mc服务器了。</p>
<p>若路由器没有内置任何ddns的支持，还可以在服务器的主机上使用<a href="https://github.com/jeessy2/ddns-go">ddns-go</a>程序。</p>
<br/><p>访问 <a href="https://krau.top/posts/mc-host-server/index" target="_blank">https://krau.top/posts/mc-host-server/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-06-20T10:00:38.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用ACME脚本申请并配置 SSL 证书]]></title>
        <id>https://krau.top/posts/acmessl/index</id>
        <link href="https://krau.top/posts/acmessl/index"/>
        <updated>2022-04-22T16:48:01.000Z</updated>
        <content type="html"><![CDATA[<h2>为什么选择ACME</h2>
<p>要想通过 TLS 访问自己域名下的服务, 就必须申请证书。类似宝塔这种面板提供了简单快捷的方法一键申请, 但终究不是良好的解决方案。一来宝塔过于臃肿, 二来其需要手机实名验证。使用各大云服务商提供的【免费证书】则还需要登录网页、申请、导入证书, 未免操作繁琐。更何况这些云服务商提供的证书只能给你很少的自由选择性。</p>
<p>而ACME则很好地解决了这些问题。</p>
<!-- more -->
<p>系统环境: Debian</p>
<h2>准备工作与脚本安装</h2>
<p>在开始之前, 首先将域名解析到服务器的ip上。</p>
<h3>安装</h3>
<p>首先安装socat, 它是acme所需的组件</p>
<pre><code class="language-shell">apt update
apt install socat -y
</code></pre>
<p>然后安装acme脚本</p>
<pre><code class="language-shell">curl https://get.acme.sh | sh
</code></pre>
<p>脚本安装在用户目录下的<code>.acme.sh</code>（隐藏）文件夹内, 使用 <code>. .bashrc</code> 让acme.sh命令生效</p>
<p>接下来使用脚本申请证书</p>
<h2>证书申请</h2>
<p>首先注册CA账户, 将下列命令中的邮箱改为自己的邮箱地址</p>
<pre><code class="language-shell">acme.sh --register-account -m xxxx@xxxx.com
</code></pre>
<p>注册成功的输出结果如下</p>
<p><img src="https://krau.top/assets/acmessl1.Ceg50DXy.webp" alt="1">_注册成功</p>
<p>然后申请证书, 接下来的方法有多种, 根据自己情况选择可用的一种即可</p>
<h3>监听80端口申请证书</h3>
<p>如果服务器的80端口空闲, 且能够访问, 可以让acme临时监听80端口, 完成验证。执行以下命令即可。将domain.com改为自己的域名</p>
<pre><code class="language-shell">acme.sh --issue -d domain.com --standalone
</code></pre>
<h3>通过Nginx服务申请</h3>
<p>如果服务器上已经安装了nginx, 且网页能够正常打开, 执行以下命令即可通过nginx验证并申请证书。</p>
<pre><code class="language-shell">acme.sh --issue -d mydomain.com --nginx
</code></pre>
<hr>
<p>如果成功申请了证书, 将会看到的输出结果是一长串自己的证书和证书文件存放路径。</p>
<h2>安装证书到指定路径</h2>
<p>生成的证书文件默认存放在与acme.sh脚本相同文件夹内, 大多数情况下不应直接使用该路径下的证书文件, 而使用以下命令安装证书到指定路径。</p>
<pre><code class="language-shell">acme.sh --install-cert -d domain.com --key-file &lt;path&gt;privkey.pem --fullchain-file &lt;path&gt;fullchain.pem
</code></pre>
<p>其中<code>--key-file &lt;path&gt;privkey.pem</code>指定的为私钥文件, <code>--fullchain-file &lt;path&gt;fullchain.pem</code>为证书文件。</p>
<p>done</p>
<h2>善后工作</h2>
<p>在安装acme.sh脚本时, 它自动为创建 cronjob, 每天 0:00 点自动检测所有的证书, 如果快过期了, 需要更新, 则会自动更新证书。<br>
但由于acme 协议和 letsencrypt CA 都在频繁的更新, 因此 <a href="http://acme.sh">acme.sh</a> 也应当经常更新以保持同步。参考以下命令</p>
<pre><code class="language-shell">acme.sh --upgrade --auto-upgrade  #开启acme的自动更新
acme.sh --upgrade --auto-upgrade 0  #关闭自动更新
</code></pre>
<br/><p>访问 <a href="https://krau.top/posts/acmessl/index" target="_blank">https://krau.top/posts/acmessl/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-04-22T16:48:01.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用官方脚本一键安装docker|安装docker-compose]]></title>
        <id>https://krau.top/posts/install-docker-one-key</id>
        <link href="https://krau.top/posts/install-docker-one-key"/>
        <updated>2022-04-20T14:29:38.000Z</updated>
        <content type="html"><![CDATA[<h2>安装docker</h2>
<p>docker官方提供了傻瓜式安装脚本，为你做好所有工作，免去了手动安装的繁琐。</p>
<p>本文实测系统环境：debian11</p>
<p>执行以下命令</p>
<p>::: tip 2023-03-07</p>
<p>不知道从什么时候开始官方脚本已经默认也安装了 <code>docker compose</code>, 不需要后面的手动安装了, 因此已删除多余的内容</p>
<p>也就是说, 只需要执行下面的一键脚本</p>
<p>注意是 <code>docker compose</code> 而不是 <code>docker-compose</code>. 执行命令时候也没有这个杠</p>
<p>:::</p>
<pre><code class="language-shell">curl -fsSL https://get.docker.com | bash -s docker
</code></pre>
<p>可在此命令后附带<code>--mirror</code>参数设置镜像源，以提高国内服务器下载docker的速度</p>
<p>如使用阿里云镜像:</p>
<pre><code class="language-shell">curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
</code></pre>
<!-- more --><br/><p>访问 <a href="https://krau.top/posts/install-docker-one-key" target="_blank">https://krau.top/posts/install-docker-one-key</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-04-20T14:29:38.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用Linuxdeploy将旧手机改造为服务器]]></title>
        <id>https://krau.top/posts/old-phone-become-server/index</id>
        <link href="https://krau.top/posts/old-phone-become-server/index"/>
        <updated>2022-01-24T19:34:31.000Z</updated>
        <content type="html"><![CDATA[<p>最近因疫情闲在家，就折腾起了自己的旧手机</p>
<!-- more -->
<h2>准备</h2>
<ol>
<li>一部已经获得root权限的手机 <s>若是ov华等无法获取root的手机，建议拿去换剪刀</s> ，本文以 redmi note7 pro 为例，已刷机安卓11类原生</li>
<li>一台能够使用ssh终端的设备</li>
</ol>
<h2>安装linuxdeploy并配置</h2>
<p>下载 <a href="https://github.com/meefik/linuxdeploy/releases">linuxdeploy</a>，安装到旧手机</p>
<blockquote>
<p>请确保安装的linuxdeploy版本≥2.0，否则需要首先安装busybox。而2.0以上的linuxdeploy内置了busybox</p>
</blockquote>
<p>打开linuxdeploy软件，点击左上角按钮，选择设置，勾选【锁定wi-fi】与【CPU唤醒】，其他选项按需配置，亦可保持默认</p>
<p>回到上一级页面，点击右下角按钮，进行以下配置：</p>
<h2>配置</h2>
<h3>引导设置</h3>
<ul>
<li>【发行版GNU/Linux】 建议选择Debian或centos，它们在linuxdeploy运行的兼容性更好</li>
<li>【架构】一般都选择arm64（Debian）或aarch64（centos），软件也会自动识别适合你的手机的架构，若特殊情况请搜索关键词 ‘{你手机的cpu}+架构’</li>
<li>【发行版GNU/Linux版本】按需选择，一般选择buster或stable</li>
<li>【源地址】建议改为国内源：<a href="http://mirrors.163.com/debian">http://mirrors.163.com/debian</a> ，保持默认亦可</li>
<li>【安装类型】按需设置，建议选择镜像文件，因为它是一个单镜像文件</li>
<li>【安装路径】默认为<code>${EXTERNAL_STORAGE}/linux.img</code>，一般保持默认即可</li>
<li>【镜像大小】按需设置，若需搭建网盘服务，建议设置较大大小，可以使文件有足够的空间存储在linux镜像空间内，省去挂载手机存储的麻烦</li>
<li>【文件系统】ext4</li>
<li>【用户名】自行设置，建议设置为 root</li>
<li>【用户密码】自行设置</li>
<li>【特权用户】按需设置，一般保持默认</li>
<li>【本地化】简体中文选择zh_CN.UTF-8，按需设置</li>
<li>【DNS】按需设置，或保持默认</li>
</ul>
<h3>初始化</h3>
<ul>
<li>【启用】勾选以启用初始化功能</li>
<li>【初始化系统】选择sysv</li>
<li>【初始化设置】视情况设置，或保持默认</li>
</ul>
<h3>挂载</h3>
<p>Source：手机目录<br>
Target：Linux目录<br>
使用能够查看到真实路径的文件浏览器，复制需要挂载的目录到挂载点列表，填入第一行即可</p>
<h3>SSH</h3>
<p>务必勾选启用SSH服务器，SSH设置按需配置</p>
<h3>声音服务</h3>
<p>按需配置</p>
<h3>图形界面</h3>
<p>按需配置</p>
<hr>
<p>配置完成后，返回上一级，点击右上角按钮，点击安装，等待出现<code>&lt;&lt;&lt; deploy</code>即安装完成。</p>
<p>点击停止按钮并确定，再点击启动，出现<code>&lt;&lt;&lt; start</code>即启动成功</p>
<p>然后即可使用同一局域网下的ssh工具连接</p>
<p>为了防止旧手机的内网ip地址变更，在路由设置页面进行ip与MAC绑定，并把手机wifi连接中的隐私设置相关选项改为 使用设备MAC</p>
<p>连接后，安装常用工具</p>
<p><code>apt install wget vim curl zip -y</code></p>
<p>附：<br>
xshell工具家庭/学校版下载连接<br>
<a href="https://www.xshell.com/zh/free-for-home-school/">https://www.xshell.com/zh/free-for-home-school/</a></p>
<h2>安装并配置cloudreve</h2>
<p>项目发行地址：<a href="https://github.com/cloudreve/Cloudreve/releases">cloudreve</a></p>
<p>截止到撰写本文（2022.01），最新版本为3.4.2</p>
<p>执行以下命令</p>
<p>切换到用户目录</p>
<p><code>cd /opt</code></p>
<p>创建文件夹并切换</p>
<p><code>mkdir cloudreve&amp;&amp;cd cloudreve</code></p>
<p>下载cloudreve</p>
<p><code>wget https://github.com/cloudreve/Cloudreve/releases/download/3.4.2/cloudreve_3.4.2_linux_arm64.tar.gz</code></p>
<p>根据需求自行选择下载的版本并替换wget后的链接</p>
<p>解压</p>
<p><code>tar -zxvf cloudreve_3.4.2_linux_arm64.tar.gz</code></p>
<p>赋予权限</p>
<p><code>chmod +x ./cloudreve</code></p>
<p>启动</p>
<p><code>./cloudreve</code></p>
<p>在终端中会给出初始管理员账号与密码，使用该账号登录</p>
<p>若无法访问，查看防火墙是否开放端口</p>
<p>进入cloudreve后，点击右上角头像，进入管理面板，即可进行相关设置，具体参见<a href="https://docs.cloudreve.org/">cloudreve官方文档</a></p>
<p>需要注意的是，在配置使用本机存储策略的用户组时，若存储目录在镜像空间内，要确保【初始容量】小于【镜像大小】，否则会无法上传任何文件</p>
<h2>通过aria2实现离线下载</h2>
<p>cloudreve内置了依赖于aria2的离线下载功能，因此需先进行aria2的配置</p>
<h3>安装并配置aria2</h3>
<p>安装</p>
<p><code>apt install aria2 -y</code></p>
<p>下载由<a href="https://github.com/P3TERX">P3TERX</a>提供的aria2配置文件（亦可选择其他配置或参考<a href="https://aria2.github.io/">aria2官方文档</a>自行创建）</p>
<pre><code class="language-shell">cd
wget https://github.com/P3TERX/aria2.conf/archive/master.zip
unzip master.zip
</code></pre>
<p>将解压出的文件夹重命名为.aria2</p>
<p><code>mv aria2.conf-master .aria2</code></p>
<p>修改aria2配置文件</p>
<p><code>cd .aria2&amp;&amp;vim aria2.conf</code></p>
<p><a href="https://github.com/P3TERX">P3TERX</a> 的配置文件包含了中文注释，根据注释按需修改即可，一般需要改动的是<strong>下载目录</strong>、<strong>RPC密钥</strong>与<strong>BT监听端口</strong>，其他保持默认即可</p>
<p>配置修改完成后，在.aria2目录内，执行<code>./tracker.sh</code>即可更新tracker</p>
<p>若更新失败，可进行手动更新，参考以下tracker列表</p>
<p><a href="https://trackerslist.com/all_aria2.txt">https://trackerslist.com/all_aria2.txt</a></p>
<p>将列表内容复制到<code>aria2.conf</code>中的<code>bt-tracker</code>参数后即可</p>
<p>创建aria2.session文件</p>
<p><code>touch aria2.session</code></p>
<p>测试aria2能否成功启动</p>
<p><code>aria2c --conf-path=/root/.aria2/aria2.conf</code></p>
<p>第一次启动需要指定配置文件，日后启动只需使用<code>aria2c</code>即可</p>
<p>若启动成功，关闭即可，接下来进行进程保护配置，方便后续操作</p>
<h3>守护cloudreve与aria2进程</h3>
<p>cloudreve与aria2运行后，若关闭ssh终端，二者也会随之停止运行，因此需要进行进程守护，本文是使用 <code>supervisor</code></p>
<p>安装</p>
<pre><code class="language-shell">apt install python-setuptools -y
apt install supervisor -y
</code></pre>
<h4>cloudreve</h4>
<p>为cloudreve创建配置并打开配置文件<br>
<code>sudo vim /etc/supervisor/conf.d/cloudreve.conf</code></p>
<p>将以下内容按需修改后，写入cloudreve的supervisor配置文件，具体参见<a href="http://supervisord.org/configuration.html">supervisor官方文档</a></p>
<pre><code class="language-ini">[program:cloudreve]
directory=/opt/cloudreve
command=/opt/cloudreve/cloudreve
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/cloudreve.err
stdout_logfile=/var/log/supervisor/cloudreve.log
environment=CODENATION_ENV=prod
</code></pre>
<p>以上内容备注：<br>
程序目录=/opt/cloudreve<br>
运行命令(绝对路径)=/opt/cloudreve/cloudreve<br>
自启动=true<br>
自动重启=true<br>
错误日志保存路径=<br>
其他日志保存路径=<br>
环境=</p>
<h4>aria2</h4>
<p>与上同理</p>
<p>为aria2创建配置并打开配置文件</p>
<p><code>sudo vim /etc/supervisor/conf.d/aria2.conf</code></p>
<p>将以下内容按需修改后，写入aria2的supervisor配置文件，具体参见<a href="http://supervisord.org/configuration.html">supervisor官方文档</a></p>
<pre><code class="language-ini">[program:aria2]
directory=/root/.aria2
command=aria2c --conf-path=/root/.aria2/aria2.conf
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/aria2.err
stdout_logfile=/var/log/supervisor/aria2.log
environment=CODENATION_ENV=prod
</code></pre>
<p>以上内容备注：<br>
程序目录=/root/.aria2<br>
运行命令(绝对路径)=aria2c --conf-path=/root/.aria2/aria2.conf<br>
自启动=true<br>
自动重启=true<br>
错误日志保存路径=<br>
其他日志保存路径=<br>
环境=</p>
<hr>
<p>启动supervisor</p>
<p><code>supervisord -c /etc/supervisor/supervisord.conf</code></p>
<p>至此便完成了进程守护配置</p>
<p>如需管理supervisor内进程，参考以下指令：</p>
<pre><code class="language-shell">sudo supervisorctl start cloudreve #启动    
sudo supervisorctl stop cloudreve #停止    
sudo supervisorctl status cloudreve #状态
</code></pre>
<p>附.网页管理supervisor进程配置：</p>
<p>在supervisor配置文件中（/etc/supervisor/supervisord.conf），添加以下内容</p>
<pre><code class="language-ini">[inet_http_server]
port=127.0.0.1:8888
username=
password=
</code></pre>
<p>其中port、username、password参数可自行设置，若需外部访问，可将port设置为 <code>0.0.0.0:8888</code></p>
<h3>配置开机自启动</h3>
<p>若在linuxdeploy中，初始化系统选择了sysv，则无需额外配置即可自启动supervisor及其中的程序<br>
若使用run-parts初始化，需额外配置.</p>
<h2>配置cloudreve离线下载功能</h2>
<p>登录cloudreve管理页面，选择 离线下载节点 ，修改 主机（本机） 节点<br>
<img src="https://krau.top/assets/20220125103835.DcGjQQ4u.png" alt=""></p>
<ol>
<li>启用离线下载</li>
<li>参考配置</li>
<li>RPC服务地址，由于cloudreve与aria2运行在同一台服务器（旧手机）上，故填写127.0.0.1:{prot}</li>
</ol>
<p><img src="https://krau.top/assets/20220125103937.DKJQfAZ7.png" alt=""></p>
<ol>
<li>RPC令牌，与 Aria2 配置文件中rpc-secret保持一致</li>
<li>临时下载目录，填写绝对路径，并保证cloudreve具有此目录的权限</li>
<li>额外参数配置，一般保持默认</li>
</ol>
<p>完成配置后，点击下一步，修改杂项信息，再点击下一步，即可保存该节点配置</p>
<p>然后点击用户组，编辑用户组配置，允许进行离线下载<br>
<img src="https://krau.top/assets/20220125104705.DFjbgAic.png" alt=""></p>
<hr>
<p>至此，所有配置完成，可在同一局域网下，打开cloudreve页面测试是否成功实现了个人云盘与离线下载功能</p>
<br/><p>访问 <a href="https://krau.top/posts/old-phone-become-server/index" target="_blank">https://krau.top/posts/old-phone-become-server/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2022-01-24T19:34:31.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[ray-mmd制作法线贴图]]></title>
        <id>https://krau.top/posts/mmdray-normal-map/index</id>
        <link href="https://krau.top/posts/mmdray-normal-map/index"/>
        <updated>2021-08-13T08:59:08.000Z</updated>
        <content type="html"><![CDATA[<p>如何使材质贴图更有层次感？</p>
<p>这就需要制作相应的<a href="https://zh.wikipedia.org/wiki/%E6%B3%95%E7%BA%BF%E8%B4%B4%E5%9B%BE">法线贴图</a></p>
<p>本文以YYB式初音未来为例，制作相应的法线贴图，使用ray渲染的版本为1.5.2</p>
<!-- more -->
<hr>
<p>在制作法线贴图之前，首先要在MMD导入相应的模型，并完成ray渲染的基本流程，方便以下的对比操作，以寻找合适的参数。</p>
<p><img src="https://krau.top/assets/20210813091507.tLcVdMDh.png" alt=""></p>
<p>在模型的目录中，找到需要制作的贴图，文件夹名一般为<code>Texture</code></p>
<p>然后使用ps导入贴图，选择滤镜&gt;3D&gt;生成法线图<br>
<img src="https://krau.top/assets/20210813093103.BUpMALhT.png" alt=""></p>
<p>导出为png格式，文件名应是英文</p>
<p>在<code>~/ray-mmd-1.5.2/Materials</code>目录中，找到<code>material_2.0.fx</code>、<code>material_common_2.0.fxsub</code>两个文件，复制到模型目录中的材质贴图文件夹</p>
<p><img src="https://krau.top/assets/20210813093713.CZZ8XC7O.png" alt=""></p>
<p>然后重命名<code>material_2.0.fx</code>文件为<code>{你所导出的法线贴图的文件名}.fx</code></p>
<p><img src="https://krau.top/assets/20210813094047.DGrNpap2.png" alt=""></p>
<p>打开重命名后的该fx文件，在第28行左右找到<code>#define NORMAL_MAP_FROM 0</code>，将0修改为1以启用法线贴图。并更改<code>#define NORMAL_MAP_FILE &quot;normal.png&quot; </code>中的&quot;normal&quot;为法线贴图文件名</p>
<p><img src="https://krau.top/assets/20210813094408.6dranVqE.png" alt=""></p>
<p>然后保存，在mmd中，在<code>MaterialMap</code>栏中，展开所修改的模型，找到对应的贴图，为其添加法线贴图效果</p>
<p><img src="https://krau.top/assets/20210813094739.CzZJXu9m.png" alt=""></p>
<p>此时可以对照mmd中的效果调整fx文件中的参数，修改参数后保存文件，即可在mmd中预览到效果。</p>
<p>几个参数的作用：</p>
<ul>
<li>
<p>const float normalMapScale = 1.0;<br>
法线效果强度，该参数越大，层次感越明显（下图为将该参数设置为5.0的效果）<br>
<img src="https://krau.top/assets/20210813095248.C6ALoMTK.png" alt=""></p>
</li>
<li>
<p>#define SMOOTHNESS_MAP_TYPE 0<br>
法线效果平滑度，该参数越大，发现效果显示越平滑，同时颗粒感与湿润感也会更强（多用于场景）</p>
</li>
</ul>
<hr>
<p>tips：</p>
<ol>
<li>如果需要为同一模型的多个材质贴图添加法线贴图效果，只需要为每个法线贴图复制一个<code>material_2.0.fx</code>文件，并重命名为对应的文件名，而<code>material_common_2.0.fxsub</code>只在材质文件夹中保留一份即可</li>
<li>场景制作法线贴图与人物模型同理，但参数的调整与人物模型区别较大，需根据不同的需求进行尝试</li>
</ol>
<br/><p>访问 <a href="https://krau.top/posts/mmdray-normal-map/index" target="_blank">https://krau.top/posts/mmdray-normal-map/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2021-08-13T08:59:08.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[ray-mmd内置天空盒调整参数简介]]></title>
        <id>https://krau.top/posts/mmdray-skyboxs/index</id>
        <link href="https://krau.top/posts/mmdray-skyboxs/index"/>
        <updated>2021-08-12T19:57:39.000Z</updated>
        <content type="html"><![CDATA[<p>完成ray渲染的基本流程后，就要开始调各种参数了，本篇介绍ray渲染内置的几个天空盒的参数<br>
<img src="https://krau.top/posts/mmdray-skyboxs/20210812200556.png" alt="内置天空盒"></p>
<p>在<code>模型操作</code>栏中，选择导入的天空盒，在表情栏即可进行其相关参数调整。</p>
<p>以下介绍中，把表情四栏按从左到右，从上到下分成 第一栏、第二栏、第三栏、第四栏。</p>
<h2>Helipad GoldenHour</h2>
<h3>第一栏</h3>
<ul>
<li>Balance<br>
可以看到该栏中有BalanceR+-/G+-/B+-以及Gray+的调整，它们即对应色调红绿蓝（RGB）三色以及灰色的调整。<br>
例如，调高BalanceR+栏，画面将偏红色</li>
</ul>
<h3>第二栏</h3>
<ul>
<li>EnvDiffLight<br>
该参数为天空盒的光源强度，调高该参数，天空盒对模型的影响将会变亮</li>
<li>EnvSpecLight<br>
该参数为漫反射强度，调高该参数，模型漫反射增强</li>
</ul>
<h3>第三栏</h3>
<ul>
<li>Diffuse<br>
该栏调整漫反射色调（RGB&amp;Gray）</li>
</ul>
<h3>第四栏</h3>
<ul>
<li>Specular<br>
该栏调整镜面反射色调（与Diffuse同理）</li>
</ul>
<h2>Sky Hemisphere</h2>
<p>该天空盒初始为纯白背景，一般适用于实景合成/唯美风渲染</p>
<h3>第一栏</h3>
<ul>
<li>Top<br>
可以看到该栏中有TopH+/S+/V+-，它们调整该天空盒的顶部色调效果</li>
</ul>
<h3>第二栏</h3>
<ul>
<li>Medium<br>
与第一栏同理，调整中部色调</li>
</ul>
<h3>第三栏</h3>
<ul>
<li>Bottom<br>
与上同理，调整底部色调</li>
</ul>
<h3>第四栏</h3>
<ul>
<li>EnvDiffLight<br>
该参数为天空盒的光源强度，调高该参数，天空盒对模型的影响将会变亮</li>
<li>EnvSpecLight<br>
该参数为漫反射强度，调高该参数，模型漫反射增强</li>
</ul>
<h2>Sky Night</h2>
<p>该天空盒与<code>Sky Hemisphere</code>几乎一样，区别是该天空盒带有夜晚的月亮和流星效果</p>
<h2>Time of day</h2>
<h3>第一栏</h3>
<ul>
<li>SunRadius<br>
调整太阳半径大小</li>
</ul>
<h3>第二栏</h3>
<ul>
<li>EnvDiffLight<br>
该参数为天空盒的光源强度，调高该参数，天空盒对模型的影响将会变亮</li>
<li>EnvSpecLight<br>
该参数为漫反射强度，调高该参数，模型漫反射增强</li>
</ul>
<h3>第三栏</h3>
<ul>
<li>MiePhase<br>
该参数可以影响丁达尔效应的强弱</li>
<li>FogRange<br>
调整雾气距离</li>
<li>FogIntensity<br>
该参数可以影响雾气效果强度</li>
</ul>
<h3>第四栏</h3>
<ul>
<li>Cloud<br>
云的密度，调高该参数，云将增多</li>
<li>CloudSpeed<br>
云的流动速度</li>
</ul>
<h2>Time of night</h2>
<p>该天空盒具有夜间星空银河与流星效果，参数调整与<code>Time of day</code>基本一致。<br>
区别是该天空盒第四栏的参数为影响星星的效果</p>
<br/><p>访问 <a href="https://krau.top/posts/mmdray-skyboxs/index" target="_blank">https://krau.top/posts/mmdray-skyboxs/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2021-08-12T19:57:39.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[ray-mmd渲染基本流程]]></title>
        <id>https://krau.top/posts/mmdray-basic-rendering/index</id>
        <link href="https://krau.top/posts/mmdray-basic-rendering/index"/>
        <updated>2020-07-17T16:48:46.000Z</updated>
        <content type="html"><![CDATA[<p><img src="https://krau.top/assets/cover.Clfoq2oX.png" alt="cover"></p>
<!-- more -->
<h2>Download</h2>
<p>前往 <a href="https://github.com/ray-cast/ray-mmd">github</a> 下载最新版ray mmd</p>
<p>注意：不仅仅是打包下载code就完事的，还需要去下载各项扩展（详见github仓库下的readme）</p>
<p><img src="https://krau.top/posts/mmdray-basic-rendering/1.avif" alt=""></p>
<p>下载完成后解压ray和各个扩展，把扩展文件分别复制到ray主目录中对应的文件夹中</p>
<h2>渲染基本流程</h2>
<p>载入人物，场景等数据后，就可以开始ray渲染了</p>
<p>但在这之前，还需要在mmd显示菜单中，关闭地面阴影显示，抗锯齿和各向异性过滤</p>
<p><img src="https://krau.top/assets/0.HUvvDbxv.webp" alt=""></p>
<p><em>因为ray是自带抗锯齿和阴影的，如果不关则会导致一些难以名状的画面和性能问题</em></p>
<p>第一步需要向mmd中丢入ray根目录下的ray.x和ray_controller.pmx，这两个分别是ray环境和ray控制器</p>
<p><img src="https://krau.top/assets/2.6z15gUOx.webp" alt=""></p>
<p>然后放入ray/skybox目录下的任意一个天空盒，它应该是以.pmx为后缀的文件。<br>
此处以<strong>Time of day</strong>为例。<br>
丢入之后在背景&gt;模型描绘顺序中调整人物，场景，和skybox的顺序<br>
这三个的正确相对顺序应为</p>
<p>skybox<br>
stage（场景）<br>
model（人物）</p>
<p><img src="https://krau.top/posts/mmdray-basic-rendering/3.avif" alt=""></p>
<p>然后在右上角MMEffect中添加渲染</p>
<p>在mian栏中，为场景和人物添加 main 文件夹内的 main.fx（主渲染）</p>
<p><img src="https://krau.top/assets/4.CLZtEVj1.jpg" alt=""></p>
<p>在Envlightmap（环境）栏中，为 skybox 添加渲染</p>
<p><img src="https://krau.top/assets/5.CEUwYCdF.webp" alt=""></p>
<p>在Fogmap（雾气）栏中，为 skybox 添加雾气效果渲染</p>
<p><img src="https://krau.top/assets/6.C14liERu.avif" alt=""></p>
<p>用方框框起来的三个均为 fogmap 对应的渲染，只需要选择一个添加就可以，一般选择 Time of fog with godray with cloud caster（名字越长越nb）</p>
<p>在materials栏中，为skybox渲染正确的材质</p>
<p><img src="https://krau.top/assets/7.DXmgEuIY.webp" alt=""></p>
<p>至此，再稍微调整一下打光和一些参数，就算完成了ray渲染的最基本步骤，现在的画面相较于未渲染的画面已经耐看多了</p>
<p>before：<br>
<img src="https://krau.top/assets/8.V4ghGPiC.webp" alt=""></p>
<p>now：<br>
<img src="https://krau.top/assets/9.DitulAaJ.avif" alt=""></p>
<p>但这只是最基本的套用渲染的流程，相较于无渲染也就是从看不了提升为了勉强能看，要想做出真正好康的mmd，仅仅这些是完全不行的</p>
<p>对于接触mmd时间不长的新手来说，没必要去详细了解每个概念，需要的是自己不断尝试，去感受那些渲染功能的具体作用。所以一开始更好的学mmd方法是，什么都不管，跟着做就好了。</p>
<br/><p>访问 <a href="https://krau.top/posts/mmdray-basic-rendering/index" target="_blank">https://krau.top/posts/mmdray-basic-rendering/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2020-07-17T16:48:46.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[简单汇总下那些推过的galgame（持续更新）]]></title>
        <id>https://krau.top/posts/gal-collection/index</id>
        <link href="https://krau.top/posts/gal-collection/index"/>
        <updated>2020-07-06T21:12:31.000Z</updated>
        <content type="html"><![CDATA[<!-- more -->
<p>个人 galgame 整理&amp;安利</p>
<p>就按照我能回想起来的先后顺序排序吧</p>
<p>反正我玩的不算多←是真话</p>
<hr>
<h3>将所有的歌献给未来的你</h3>
<p><img src="https://i.loli.net/2021/06/02/FEuSNprv2xtG3LH.png" alt=""></p>
<blockquote>
<p>未来のキミと、すべての歌に―是Supplement Time在2008年04月04日[1]发售的初音同人游戏。是Supplement Time出品的初音系列的同人游戏的第一部。其后续作有鈴が歌う、未来の音色―和未来の歌と、つながるヒトミ。<br>
详见<a href="https://zh.moegirl.org/zh-hans/%E5%B0%86%E6%89%80%E6%9C%89%E7%9A%84%E6%AD%8C%E7%8C%AE%E4%BA%8E%E6%9C%AA%E6%9D%A5%E7%9A%84%E4%BD%A0">萌娘百科</a></p>
</blockquote>
<p>应该是我玩的第一款gal？？不太确定了，反正玩的挺早的</p>
<p>粉丝向作品. 算是比较日常但是又有些波折的剧情，总之就是在大街上捡回家了只miku然后发生的种种......</p>
<h3>nekopara</h3>
<p><img src="https://pic.downk.cc/item/5f04083814195aa5946cd80d.jpg" alt=""></p>
<p>经典的猫系gal，<s>听说打个补丁会更有趣</s></p>
<h3>美少女万华镜</h3>
<p>不配图了这个emmmm</p>
<h3>哥哥，早上起床之前都要抱紧我哦</h3>
<p><img src="https://pic.downk.cc/item/5f0409ee14195aa5946d78e2.jpg" alt=""></p>
<blockquote>
<p>《哥哥，早上起床之前都要抱紧我哦！》是由Tinkle Position制作的一部Galgame。 <a href="https://zh.moegirl.org/zh-hans/%E5%93%A5%E5%93%A5%EF%BC%8C%E6%97%A9%E4%B8%8A%E8%B5%B7%E5%BA%8A%E4%B9%8B%E5%89%8D%E9%83%BD%E8%A6%81%E6%8A%B1%E7%B4%A7%E6%88%91%E5%93%A6%EF%BC%81">萌娘百科详情</a></p>
</blockquote>
<p><s>国欠妹系列</s></p>
<h3>缘之空</h3>
<p><img src="https://pic.downk.cc/item/5f040afe14195aa5946dd903.jpg" alt=""></p>
<blockquote>
<p>《缘之空》（ヨスガノソラ，Yosuga no Sora）是Sphere于2008年12月5日发售的恋爱文字冒险游戏，也是Sphere的处女作。亦有漫画、动画、Fandisk、广播剧等衍生作品。<a href="https://zh.moegirl.org/zh-hans/%E7%BC%98%E4%B9%8B%E7%A9%BA#">萌娘百科：缘之空</a></p>
</blockquote>
<p>国欠妹*2</p>
<p>对应的续作还有一部『悠之空』</p>
<h3>双子洛丽塔后宫</h3>
<p><img src="https://pic.downk.cc/item/5f040b9114195aa5946e11b6.jpg" alt=""></p>
<p>不说什么~</p>
<h3>東津萌米-穗姬</h3>
<p><img src="https://i.loli.net/2020/07/07/ygVwB4o3i19zxXm.jpg" alt=""><em>找不到更高清的图了</em></p>
<blockquote>
<p>穗姬是原产于台湾屏东县东港镇的杂交稻米品种“高雄145”以及该稻米所产的大米“东津萌米”的萌娘化形象，也登场于游戏《东津萌米-穗姬》中。 <a href="https://zh.moegirl.org/zh-hans/%E7%A9%97%E5%A7%AC#">萌娘百科：穗姬</a></p>
</blockquote>
<p>国产游戏，更偏向于养成系的（种大米），姑且也算是galgame吧</p>
<p><s>万物皆可萌</s></p>
<h3>家有仙妻～无名小猫又与神Tuber～</h3>
<p><img src="https://krau.top/assets/20200707140100.dnVGR1wx.png" alt=""></p>
<p>猫系galgame*2</p>
<p>萌百没收录这个......明明挺<s>社保</s>的</p>
<h3>恋爱少女人格崩坏</h3>
<p><img src="https://pic.downk.cc/item/5f154ec714195aa5942c10a6.jpg" alt=""></p>
<p>能搞笑又能社保~~（指男主）~~</p>
<h3>魔女的夜宴</h3>
<p>时间：2020.7.17-2020.7.24±</p>
<p><img src="https://krau.top/assets/sabbatofwitch.CoIObdcd.png" alt=""></p>
<p>谁敢反对狗头，我就打爆他的宁宁（错乱</p>
<h3>千恋万花</h3>
<p>时间：2020.7.26±-2020.7.28±（只过了从雨线）</p>
<p>从雨天下第一！</p>
<p>2026.8 二刷, 除了蕾娜其他线倒是都过了一遍.</p>
<p><img src="https://krau.top/assets/20200804182632.BweUZyLc.png" alt=""></p>
<h3>ATRI -My Dear Moments-</h3>
<p>时间：2020.7.29-2020.8.2？</p>
<p>咕噜咕噜~呸</p>
<p><img src="https://i.loli.net/2020/08/04/kEtr4q3MexlNzvA.png" alt=""></p>
<h3>星光咖啡馆与死神之蝶</h3>
<p>时间：2020.8.28-8.31（明月栞那）</p>
<p><s>星巴克与大扑棱蛾子</s></p>
<p><img src="https://i.loli.net/2020/09/11/eRyb7AahWzgdKkD.png" alt=""></p>
<h3>RIDDLEJOKER</h3>
<p>时间：2020.9.7-9.16（在原七海）</p>
<p>能推睡着的gal......但感觉还不戳，七海天下第一</p>
<p><img src="https://i.loli.net/2021/06/02/VC8r7GvWoeUTm4M.png" alt="图片.png"></p>
<h3>想要传达给你的爱恋</h3>
<p>时间：5.24-5.30</p>
<p>苟利星奏生死以，岂因新岛避趋之</p>
<p><img src="https://i.loli.net/2021/06/02/2zX4i3DFrco7Lhs.png" alt=""></p>
<h3>异想魅惑</h3>
<p>时间:2021.6.4（云）</p>
<p>勉强也算gal吧...</p>
<p>探索解密式的剧情玩法+色气可爱的唯一女主魅魔设定</p>
<p>虽然te确实很感人，但其他五条结局也很绝望和惊悚</p>
<p><img src="https://i.loli.net/2021/06/04/Bir1Gpe7KzbDxT5.png" alt=""></p>
<h3>诱惑自大洛丽塔</h3>
<p>时间：2021年</p>
<p><img src="https://i.loli.net/2021/06/04/LQpf9eHyVNYbaCM.png" alt=""></p>
<h3>9-nine 系列</h3>
<h4>九次九日九重色</h4>
<p>时间：2021.6.5</p>
<p><img src="https://i.loli.net/2021/06/06/tZD7gOkq6lzo1WR.png" alt=""></p>
<p>喵都太可爱了</p>
<h4>天色天歌天籁音</h4>
<p>时间：2021.6.6</p>
<p>国欠我一个天妹</p>
<p><img src="https://i.loli.net/2021/06/06/4Med61olIyfZv5t.png" alt=""></p>
<h4>春色春恋春熙风</h4>
<p>时间：2021.6.7</p>
<h4>雪色雪花雪余痕</h4>
<p>时间：2021.6.7-6.8</p>
<h4>9-nine- 新章</h4>
<p>时间：2021.6.8</p>
<h3>牵绊闪耀的恋之伊吕波</h3>
<p>时间：2021.8.25-8.29</p>
<p><img src="https://krau.top/assets/20210829224332.DZvJHb1x.png" alt=""></p>
<p>发椿啦发椿啦，当然椿恋歌也会推的啦，这里就只写一个好了</p>
<h3>星空列车与白的旅行</h3>
<p>时间：2021.9</p>
<p><img src="https://krau.top/assets/000_title.D3kl83NG.png" alt=""></p>
<p>一部“童话”</p>
<h3>巧可甜恋</h3>
<p>时间：2021.9</p>
<p><img src="https://krau.top/assets/20211001193405.BeeTOoh9.png" alt=""></p>
<p>剧情是真的短，但她真的太可爱了太刑了</p>
<h3>糖调！-sugarfull tempering-</h3>
<p>时间：2021.9</p>
<p><img src="https://krau.top/posts/gal-collection/Sugarful_cover.jpg" alt=""></p>
<p>冰织天下第一！</p>
<h3>保健室的老师与沉迷吹泡泡的助手</h3>
<p><img src="https://i.imgur.com/LuXnpEG.jpeg" alt=""></p>
<h3>巧可甜恋2</h3>
<p>24年暑假</p>
<h3>拔作岛1&amp;2</h3>
<p>25年8月</p>
<p><img src="https://krau.top/assets/2025-09-02-14-58-03.MJ5lXq5G.png" alt="0"></p>
<p>用荒诞搞笑的故事也能打动人心</p>
<h3>天使☆嚣嚣 RE-BOOT!</h3>
<p>26年暑假</p>
<p>Noa 可爱滴捏</p>
<p><img src="https://krau.top/assets/2026-08-27-00-49-43.DN46Ia4h.png" alt="1"></p>
<h3>橘光柠水随想曲</h3>
<p>26年暑假</p>
<p>又名 <s>蜜雪冰城</s></p>
<p>除了隐杏珠其他几位女主都不是很戳</p>
<p><img src="https://krau.top/assets/2026-08-27-00-51-56.CRZXVtVz.png" alt="2"></p>
<h3>小小的身影重叠的内心</h3>
<p>26年暑假</p>
<p>免费国产 gal , 除了配音以外没啥问题</p>
<p><img src="https://krau.top/assets/2026-08-27-00-53-48.DXVSCxpd.png" alt="3"></p>
<br/><p>访问 <a href="https://krau.top/posts/gal-collection/index" target="_blank">https://krau.top/posts/gal-collection/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2020-07-06T21:12:31.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[MMD中导出带alpha通道视频的方法]]></title>
        <id>https://krau.top/posts/mmdray-output-videos-with-alpha/index</id>
        <link href="https://krau.top/posts/mmdray-output-videos-with-alpha/index"/>
        <updated>2020-04-07T20:33:29.000Z</updated>
        <content type="html"><![CDATA[<h2>前言</h2>
<p>闲来无事在摸MMD，然后看到了这个</p>
<p><a href="https://www.bilibili.com/video/BV1r7411u7ST">【PV背景配布/动作配布】九尾三妈式初音 - お気に召すまま<br>
</a></p>
<p>感觉这个配布的挺不错，想摸一个。</p>
<p>但是碰到点问题：60fps的背景是单独的视频，需要通过后期软件把人物和背景的视频叠加起来，这就要一个带有alpha通道（透明）的人物视频，去网上找了一些方法，依旧踩了不少坑。</p>
<h2>RewriteAlpha</h2>
<p>找了很多方法，感觉就这个最简单方便。</p>
<p>首先去下载这个MME：<a href="https://bowlroll.net/file/27004">https://bowlroll.net/file/27004</a></p>
<p>这个MME可以让你随心所欲的加花里花哨的全局MME，而不会丢失透明通道</p>
<p>下载之后<strong>保持乱码状态解压</strong></p>
<p>然后在 <code>RewriteAlpha</code> 文件夹中，往MMD里加入这个 <code>RewriteAlpha.x</code></p>
<p>在MMD&gt;背景&gt;绘制附件中，把<code>RewriteAlpha.x</code>放到最后一位</p>
<p><img src="https://krau.top/posts/mmdray-output-videos-with-alpha/20200407205316.png" alt=""></p>
<p>然后打开MME面板，找到<code>alphamap</code>栏，把skybox和stage的渲染关闭（取消勾选），人物模型的渲染开启，并勾选上RewriteAlpha.x的渲染</p>
<p><img src="https://krau.top/assets/20200407205639.Ca8O6RSZ.png" alt=""></p>
<p>这样就算完成了，导出的时候将会导出只有人物渲染，且背景是透明通道的视频，也可以先导出一张png看看效果。<s>别渲染了半天发现不是透明通道，别问我怎么知道的</s></p>
<p><img src="https://krau.top/assets/alphammd.DimpNjoW.png" alt=""></p>
<br/><p>访问 <a href="https://krau.top/posts/mmdray-output-videos-with-alpha/index" target="_blank">https://krau.top/posts/mmdray-output-videos-with-alpha/index</a> 查看原文。</p>]]></content>
        <author>
            <name>Krau</name>
            <email>i@valaxy.site</email>
            <uri>https://valaxy.site/</uri>
        </author>
        <published>2020-04-07T20:33:29.000Z</published>
    </entry>
</feed>