<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>星屿浅奈 ✧ Saneko</title><description>做自己喜欢的事</description><link>https://saneko.me</link><item><title>忘记关闭自动扣费的教训</title><link>https://saneko.me/blog/316b206330f9</link><guid isPermaLink="true">https://saneko.me/blog/316b206330f9</guid><description>记录一次因疏忽导致损失千元的教训。</description><pubDate>Fri, 03 Apr 2026 09:04:08 GMT</pubDate><content:encoded>&lt;p&gt;去年 5 月，我正在折腾一个 WPF 样式库，想给 &lt;code&gt;TButton&lt;/code&gt; 组件加上好看的动画图标。找了一大圈开源素材都不太满意，最后碰巧发现了 &lt;a href=&quot;https://lordicon.com/&quot;&gt;Lordicon&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这网站的图标确实精致，但好东西基本都要 Pro 会员。为了项目效果，我咬咬牙买了一个月的 Pro。结果付完款，完全忘了去后台关掉自动续费。&lt;/p&gt;
&lt;p&gt;大半年过去了，项目早就不弄了。直到今年 3 月底看账单，才发现有一笔来路不明的扣费。点开详情一看，天塌了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_260403_091456.png&quot; alt=&quot;扣费明细&quot;&gt;&lt;/p&gt;
&lt;p&gt;就因为忘了点个取消按钮，白白扔了 1000 多块钱。&lt;/p&gt;
&lt;p&gt;这笔钱虽然不多，但也够让人肉疼好一阵子了。写下这篇博客，主要是给自己长个记性，也顺便提醒一下大家：&lt;strong&gt;不管开通什么订阅，付完钱的第一件事就是去找“Cancel Subscription”或者“Auto-renewal”把它关掉！&lt;/strong&gt; 别太相信自己的记性。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.CI8i4wYV.jpg"/><enclosure url="/_astro/thumbnail.CI8i4wYV.jpg"/></item><item><title>Waline 评论配置自定义邮件通知</title><link>https://saneko.me/blog/2c1e7fd6f7bd</link><guid isPermaLink="true">https://saneko.me/blog/2c1e7fd6f7bd</guid><description>介绍为 Waline 配置评论邮件通知的完整步骤</description><pubDate>Fri, 06 Mar 2026 15:37:50 GMT</pubDate><content:encoded>&lt;p&gt;import { LinkPreview } from &apos;astro-pure/advanced&apos;
import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;本文介绍如何为 &lt;strong&gt;Waline&lt;/strong&gt; 配置评论邮件通知，涵盖 SMTP 设置、环境变量配置与自定义邮件模板。以 &lt;strong&gt;Vercel&lt;/strong&gt; 部署为例，其他平台可对照 &lt;a href=&quot;https://waline.js.org/&quot;&gt;Waline 官方文档&lt;/a&gt; 操作。&lt;/p&gt;
&lt;h2&gt;部署 Waline&lt;/h2&gt;
&lt;p&gt;按官方文档完成 Waline 部署即可：&lt;/p&gt;
&lt;h2&gt;获取 SMTP 授权码&lt;/h2&gt;
&lt;p&gt;Waline 通过 SMTP 发送邮件，需使用&lt;strong&gt;授权码&lt;/strong&gt;而非邮箱登录密码。建议使用专门用于发信的邮箱。&lt;/p&gt;
&lt;p&gt;以 &lt;strong&gt;163 邮箱&lt;/strong&gt;为例：登录网页版 → &lt;strong&gt;设置&lt;/strong&gt; → &lt;strong&gt;POP3/SMTP/IMAP&lt;/strong&gt; → 开启 &lt;strong&gt;POP3/SMTP&lt;/strong&gt; 与 &lt;strong&gt;IMAP/SMTP&lt;/strong&gt; 服务 → 获取并保存&lt;strong&gt;授权码&lt;/strong&gt;（后续填入 &lt;code&gt;SMTP_PASS&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;配置环境变量&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;打开 &lt;a href=&quot;https://vercel.com&quot;&gt;Vercel Dashboard&lt;/a&gt;，进入你的 Waline 项目。&lt;/li&gt;
&lt;li&gt;进入 &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Environment Variables&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;Add New&lt;/strong&gt;，依次配置以下环境变量：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;| 环境变量 | 说明 | 举例 |
|----------|------|------|
| &lt;code&gt;SMTP_SERVICE&lt;/code&gt; | SMTP 服务提供商；&lt;a href=&quot;https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json&quot;&gt;邮箱支持列表&lt;/a&gt; | &lt;code&gt;163&lt;/code&gt; |
| &lt;code&gt;SMTP_HOST&lt;/code&gt; | SMTP 服务器地址，可在邮箱设置中查看。 | &lt;code&gt;smtp.163.com&lt;/code&gt; |
| &lt;code&gt;SMTP_PORT&lt;/code&gt; | SMTP 端口。 | &lt;code&gt;465&lt;/code&gt; |
| &lt;code&gt;SMTP_USER&lt;/code&gt; | SMTP 邮件发送服务的用户名，一般为登录邮箱。 | &lt;code&gt;yourname@163.com&lt;/code&gt; |
| &lt;code&gt;SMTP_PASS&lt;/code&gt; | SMTP 密码；163 邮箱须填 SMTP 授权码（见上文），不能填登录密码。 | 上一步获取的授权码 |
| &lt;code&gt;SMTP_SECURE&lt;/code&gt; | 是否使用 SSL。 | &lt;code&gt;true&lt;/code&gt; |
| &lt;code&gt;SITE_NAME&lt;/code&gt; | 网站名称，会在邮件中展示。 | &lt;code&gt;Saneko&lt;/code&gt; |
| &lt;code&gt;SITE_URL&lt;/code&gt; | 网站地址。 | &lt;code&gt;https://你的域名&lt;/code&gt; |
| &lt;code&gt;AUTHOR_EMAIL&lt;/code&gt; | 博主邮箱，用于接收新评论通知（博主自己的评论不通知）。 | &lt;code&gt;yourname@163.com&lt;/code&gt; |
| &lt;code&gt;SENDER_NAME&lt;/code&gt; | 自定义发件人名称。 | &lt;code&gt;Saneko&lt;/code&gt; |
| &lt;code&gt;SENDER_EMAIL&lt;/code&gt; | 自定义发件地址，通常与 &lt;code&gt;SMTP_USER&lt;/code&gt; 一致。 | &lt;code&gt;yourname@163.com&lt;/code&gt; |&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;保存所有变量后，在项目中选择 &lt;strong&gt;Redeploy&lt;/strong&gt; 重新部署，使环境变量生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;自定义邮件模板&lt;/h2&gt;
&lt;p&gt;以下模板支持亮色/暗色自适应，可按需修改样式后填入对应环境变量。&lt;/p&gt;
&lt;h3&gt;新评论通知（发给博主）&lt;/h3&gt;
&lt;p&gt;在环境变量中添加 &lt;code&gt;MAIL_TEMPLATE&lt;/code&gt;，值为下方 HTML（整段复制，保留为单段即可）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div style=&quot;margin: 0; padding: 40px 0; background-color: #fcfcfd; color: #09090b; font-family: Satoshi, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto, &apos;Helvetica Neue&apos;, Arial, sans-serif;&quot;&gt;
    &amp;#x3C;style&gt;
        /* Dark mode support */
        @media (prefers-color-scheme: dark) {
            .email-body { background-color: #0B0B10 !important; color: #f4f4f5 !important; }
            .card { background-color: #18181b !important; border-color: #27272a !important; }
            .text-muted { color: #a1a1aa !important; }
            .bg-muted { background-color: #27272a !important; }
            .text-primary { color: #8C766D !important; }
        }
    &amp;#x3C;/style&gt;
    
    &amp;#x3C;!-- Main Container --&gt;
    &amp;#x3C;div class=&quot;email-body&quot; style=&quot;max-width: 600px; margin: 0 auto; padding: 0 20px;&quot;&gt;
        
        &amp;#x3C;!-- Card --&gt;
        &amp;#x3C;div class=&quot;card&quot; style=&quot;background-color: #ffffff; border: 1px solid #e4e4e7; border-radius: 16px; overflow: hidden; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);&quot;&gt;
            
            &amp;#x3C;!-- Brand Header Inside Card --&gt;
            &amp;#x3C;div style=&quot;text-align: center; padding: 32px 0 0 0;&quot;&gt;
                &amp;#x3C;a href=&quot;{{site.url}}&quot; style=&quot;text-decoration: none; color: #09090b; font-weight: 600; font-size: 20px; letter-spacing: -0.5px;&quot;&gt;
                    &amp;#x3C;span style=&quot;color: #8C766D;&quot;&gt;✦&amp;#x3C;/span&gt; {{site.name}}
                &amp;#x3C;/a&gt;
            &amp;#x3C;/div&gt;

            &amp;#x3C;!-- Header --&gt;
            &amp;#x3C;div style=&quot;padding: 24px 24px 0 24px;&quot;&gt;
                &amp;#x3C;div style=&quot;display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;&quot;&gt;
                    &amp;#x3C;h1 style=&quot;margin: 0; font-size: 18px; font-weight: 600; color: #09090b; display: flex; align-items: center; gap: 8px;&quot;&gt;
                        &amp;#x3C;!-- MessageSquarePlus Icon --&gt;
                        &amp;#x3C;img src=&quot;https://api.iconify.design/lucide:message-square-plus.svg?color=%238C766D&quot; width=&quot;20&quot; height=&quot;20&quot; style=&quot;vertical-align: text-bottom;&quot; alt=&quot;&quot;&gt;
                        &amp;#x3C;span&gt;新的评论&amp;#x3C;/span&gt;
                    &amp;#x3C;/h1&gt;
                    &amp;#x3C;span style=&quot;font-size: 12px; font-weight: 500; background-color: #f4f4f5; color: #71717a; padding: 4px 10px; border-radius: 9999px;&quot;&gt;NEW&amp;#x3C;/span&gt;
                &amp;#x3C;/div&gt;
                &amp;#x3C;p class=&quot;text-muted&quot; style=&quot;margin: 0; font-size: 14px; color: #71717a; line-height: 1.5;&quot;&gt;
                    有人在 &amp;#x3C;strong&gt;{{site.name}}&amp;#x3C;/strong&gt; 留下了新的足迹。
                &amp;#x3C;/p&gt;
            &amp;#x3C;/div&gt;

            &amp;#x3C;!-- Content --&gt;
            &amp;#x3C;div style=&quot;padding: 24px;&quot;&gt;
                
                &amp;#x3C;!-- Comment Block --&gt;
                &amp;#x3C;div class=&quot;bg-muted&quot; style=&quot;background-color: #f4f4f5; border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid transparent;&quot;&gt;
                    &amp;#x3C;div style=&quot;font-size: 15px; color: #09090b; line-height: 1.6;&quot;&gt;
                        {{self.comment | safe}}
                    &amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;

                &amp;#x3C;!-- Meta Info --&gt;
                &amp;#x3C;div style=&quot;display: flex; flex-direction: column; gap: 12px; font-size: 13px; color: #71717a; border-top: 1px dashed #e4e4e7; padding-top: 20px;&quot;&gt;
                    &amp;#x3C;div style=&quot;display: flex; align-items: center; gap: 8px;&quot;&gt;
                        &amp;#x3C;img src=&quot;https://api.iconify.design/lucide:user.svg?color=%23a1a1aa&quot; width=&quot;16&quot; height=&quot;16&quot; alt=&quot;&quot;&gt;
                        &amp;#x3C;span&gt;&amp;#x3C;strong&gt;{{self.nick}}&amp;#x3C;/strong&gt; ({{self.mail}})&amp;#x3C;/span&gt;
                    &amp;#x3C;/div&gt;
                    &amp;#x3C;div style=&quot;display: flex; align-items: center; gap: 8px;&quot;&gt;
                        &amp;#x3C;img src=&quot;https://api.iconify.design/lucide:link.svg?color=%23a1a1aa&quot; width=&quot;16&quot; height=&quot;16&quot; alt=&quot;&quot;&gt;
                        &amp;#x3C;a href=&quot;{{site.postUrl}}&quot; style=&quot;color: #71717a; text-decoration: none; border-bottom: 1px solid #e4e4e7; padding-bottom: 1px;&quot;&gt;{{site.postUrl}}&amp;#x3C;/a&gt;
                    &amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;

                &amp;#x3C;!-- Action Button --&gt;
                &amp;#x3C;div style=&quot;margin-top: 32px;&quot;&gt;
                    &amp;#x3C;a href=&quot;{{site.postUrl}}&quot; style=&quot;display: inline-block; background-color: #09090b; color: #ffffff; text-decoration: none; padding: 10px 24px; border-radius: 9999px; font-size: 14px; font-weight: 500; transition: opacity 0.2s;&quot;&gt;
                        查看并回复
                    &amp;#x3C;/a&gt;
                &amp;#x3C;/div&gt;

            &amp;#x3C;/div&gt;
            
            &amp;#x3C;!-- Footer in Card --&gt;
            &amp;#x3C;div class=&quot;bg-muted&quot; style=&quot;background-color: #fafafa; border-top: 1px solid #e4e4e7; padding: 24px; font-size: 12px; color: #a1a1aa; text-align: center;&quot;&gt;
                &amp;#x3C;p style=&quot;margin: 0;&quot;&gt;此邮件由系统发送，请不要回复此邮件&amp;#x3C;/p&gt;
            &amp;#x3C;/div&gt;

        &amp;#x3C;/div&gt;

    &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;回复通知（发给被回复用户）&lt;/h3&gt;
&lt;p&gt;在环境变量中添加 &lt;code&gt;MAIL_TEMPLATE_ADMIN&lt;/code&gt;，值为下方 HTML（当有人回复评论时，被回复者将收到此模板邮件）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div style=&quot;margin: 0; padding: 40px 0; background-color: #fcfcfd; color: #09090b; font-family: Satoshi, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto, &apos;Helvetica Neue&apos;, Arial, sans-serif;&quot;&gt;
    &amp;#x3C;style&gt;
        /* Dark mode support */
        @media (prefers-color-scheme: dark) {
            .email-body { background-color: #0B0B10 !important; color: #f4f4f5 !important; }
            .card { background-color: #18181b !important; border-color: #27272a !important; }
            .text-muted { color: #a1a1aa !important; }
            .bg-muted { background-color: #27272a !important; }
            .text-primary { color: #8C766D !important; }
        }
    &amp;#x3C;/style&gt;
    
    &amp;#x3C;!-- Main Container --&gt;
    &amp;#x3C;div class=&quot;email-body&quot; style=&quot;max-width: 600px; margin: 0 auto; padding: 0 20px;&quot;&gt;
        
        &amp;#x3C;!-- Card --&gt;
        &amp;#x3C;div class=&quot;card&quot; style=&quot;background-color: #ffffff; border: 1px solid #e4e4e7; border-radius: 16px; overflow: hidden; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);&quot;&gt;
            
            &amp;#x3C;!-- Brand Header Inside Card --&gt;
            &amp;#x3C;div style=&quot;text-align: center; padding: 32px 0 0 0;&quot;&gt;
                &amp;#x3C;a href=&quot;{{site.url}}&quot; style=&quot;text-decoration: none; color: #09090b; font-weight: 600; font-size: 20px; letter-spacing: -0.5px;&quot;&gt;
                    &amp;#x3C;span style=&quot;color: #8C766D;&quot;&gt;✦&amp;#x3C;/span&gt; {{site.name}}
                &amp;#x3C;/a&gt;
            &amp;#x3C;/div&gt;

            &amp;#x3C;!-- Header --&gt;
            &amp;#x3C;div style=&quot;padding: 24px 24px 0 24px;&quot;&gt;
                &amp;#x3C;h1 style=&quot;margin: 0; font-size: 18px; font-weight: 600; color: #09090b; display: flex; align-items: center; gap: 8px;&quot;&gt;
                    &amp;#x3C;!-- MessageCircle Icon --&gt;
                    &amp;#x3C;img src=&quot;https://api.iconify.design/lucide:message-circle.svg?color=%238C766D&quot; width=&quot;20&quot; height=&quot;20&quot; style=&quot;vertical-align: text-bottom;&quot; alt=&quot;&quot;&gt;
                    &amp;#x3C;span&gt;收到回复&amp;#x3C;/span&gt;
                &amp;#x3C;/h1&gt;
                &amp;#x3C;p class=&quot;text-muted&quot; style=&quot;margin: 8px 0 0 0; font-size: 14px; color: #71717a; line-height: 1.5;&quot;&gt;
                    你好，你在 &amp;#x3C;strong&gt;{{site.name}}&amp;#x3C;/strong&gt; 的留言收到了新的回复。
                &amp;#x3C;/p&gt;
            &amp;#x3C;/div&gt;

            &amp;#x3C;!-- Content --&gt;
            &amp;#x3C;div style=&quot;padding: 24px;&quot;&gt;
                
                &amp;#x3C;!-- Quote Block (User&apos;s Comment) --&gt;
                &amp;#x3C;div class=&quot;bg-muted&quot; style=&quot;background-color: #f4f4f5; border-radius: 12px; padding: 16px; margin-bottom: 24px; border: 1px solid transparent;&quot;&gt;
                    &amp;#x3C;div class=&quot;text-muted&quot; style=&quot;font-size: 12px; font-weight: 500; color: #71717a; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px;&quot;&gt;You&amp;#x3C;/div&gt;
                    &amp;#x3C;div style=&quot;font-size: 14px; color: #09090b; line-height: 1.6; font-style: italic;&quot;&gt;
                        {{parent.comment | safe}}
                    &amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;

                &amp;#x3C;!-- Reply Block (Admin&apos;s Reply) --&gt;
                &amp;#x3C;div style=&quot;position: relative; padding-left: 16px; border-left: 2px solid #8C766D;&quot;&gt;
                    &amp;#x3C;div style=&quot;font-size: 12px; font-weight: 600; color: #8C766D; margin-bottom: 8px;&quot;&gt;{{self.nick}}&amp;#x3C;/div&gt;
                    &amp;#x3C;div style=&quot;font-size: 15px; color: #09090b; line-height: 1.6;&quot;&gt;
                        {{self.comment | safe}}
                    &amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;

                &amp;#x3C;!-- Action Button --&gt;
                &amp;#x3C;div style=&quot;margin-top: 32px;&quot;&gt;
                    &amp;#x3C;a href=&quot;{{site.postUrl}}&quot; style=&quot;display: inline-block; background-color: #09090b; color: #ffffff; text-decoration: none; padding: 10px 20px; border-radius: 9999px; font-size: 14px; font-weight: 500; transition: opacity 0.2s;&quot;&gt;
                        查看完整对话
                    &amp;#x3C;/a&gt;
                &amp;#x3C;/div&gt;

            &amp;#x3C;/div&gt;
            
            &amp;#x3C;!-- Footer in Card --&gt;
            &amp;#x3C;div class=&quot;bg-muted&quot; style=&quot;background-color: #fafafa; border-top: 1px solid #e4e4e7; padding: 24px; font-size: 12px; color: #a1a1aa; text-align: center;&quot;&gt;
                &amp;#x3C;p style=&quot;margin: 0;&quot;&gt;此邮件由系统发送，请不要回复此邮件&amp;#x3C;/p&gt;
            &amp;#x3C;/div&gt;

        &amp;#x3C;/div&gt;

    &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加或修改上述环境变量后，需在 Vercel 中 &lt;strong&gt;Redeploy&lt;/strong&gt; 一次方可生效。&lt;/p&gt;
&lt;h2&gt;效果预览&lt;/h2&gt;
&lt;p&gt;配置完成并重新部署后，新评论与回复将触发如下样式的邮件：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新评论通知（博主收到）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_260306_211643.png&quot; alt=&quot;管理员邮件预览&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回复通知（被回复者收到）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_260306_211826.png&quot; alt=&quot;评论邮件预览&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.COaCvHMc.jpg"/><enclosure url="/_astro/thumbnail.COaCvHMc.jpg"/></item><item><title>minecraft服务器被入侵</title><link>https://saneko.me/blog/dc84dd81fc73</link><guid isPermaLink="true">https://saneko.me/blog/dc84dd81fc73</guid><description>记录一次 Minecraft 服务器被恶意玩家入侵的经历</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { WeChatChat } from &apos;astro-pure/user&apos;
import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;事情的起因&lt;/h2&gt;
&lt;p&gt;11月2号那天，一个很久没联系的大学室友突然找我，说想和他女朋友一起玩 Minecraft，问我有没有服务器。我之前的服务器早就停了，不过手头还有台闲置的华为云服务器，正好可以拿来用。&lt;/p&gt;
&lt;p&gt;那就搞呗！我就在华为云上重新搭了个原版生存服务器，用的 fabric，加了一些生电模组，像 tweakoo 这些。&lt;/p&gt;
&lt;p&gt;当时只开了正版验证，想着应该问题不大，就没开白名单。&lt;/p&gt;
&lt;p&gt;刚开始一切都很顺利，我们按部就班地建了刷铁机、村民交易所、熔炉组，还搞了沼泽刷怪塔和守卫者农场，玩得挺开心的。&lt;/p&gt;
&lt;h2&gt;被入侵了&lt;/h2&gt;
&lt;p&gt;结果到了11月23号晚上，出事了...&lt;/p&gt;
&lt;p&gt;&amp;#x3C;WeChatChat
title=&quot;ldy&quot;
messages={[
{ name: &apos;piati6666&apos;, avatar: &apos;minecraft&apos;, content: &apos;家被偷了&apos;, time: &apos;23:31&apos; },
{ name: &apos;ATaoYan&apos;, avatar: &apos;minecraft&apos;, content: &apos;？&apos;, time: &apos;23:31&apos; },
{ name: &apos;piati6666&apos;, avatar: &apos;minecraft&apos;, content: &apos;回档吧&apos;, image: &apos;https://cdn.blog.saneko.me/Blog/blog_251123_233931.jpg&apos;, time: &apos;23:32&apos; },
{ name: &apos;ATaoYan&apos;, avatar: &apos;minecraft&apos;, content: &apos;这几个哪来的&apos;, time: &apos;23:33&apos; },
{ name: &apos;piati6666&apos;, avatar: &apos;minecraft&apos;, content: &apos;我还以为你朋友&apos;, time: &apos;23:33&apos; },
]}
/&gt;&lt;/p&gt;
&lt;p&gt;当时已经晚上11点多了，我正准备睡觉。看到这个消息人都傻了，赶紧先把服务器关了，免得被破坏得更严重。&lt;/p&gt;
&lt;p&gt;第二天还要上班，我就让室友先上号看看情况。结果一看，存档被毁得不成样子，刷铁机、农场、仓库全都被拆了，东西也被拿光了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_251124_100655.png&quot; alt=&quot;守卫者农场收集&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_251124_100603.png&quot; alt=&quot;仓库&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_251124_100511.png&quot; alt=&quot;基地全景&quot;&gt;&lt;/p&gt;
&lt;h2&gt;处理结果&lt;/h2&gt;
&lt;p&gt;下班回家后，我第一件事就是把白名单开了。还好之前加了备份脚本，用 &lt;code&gt;!!pb back&lt;/code&gt; 把存档恢复到被入侵前的状态，不然真的就白玩了。&lt;/p&gt;
&lt;p&gt;这次也算是给我自己提了个醒：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_251207_221634.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Ccr80FcZ.jpg"/><enclosure url="/_astro/thumbnail.Ccr80FcZ.jpg"/></item><item><title>DeepSeek-Commit-Tool</title><link>https://saneko.me/blog/22cf2662ff62</link><guid isPermaLink="true">https://saneko.me/blog/22cf2662ff62</guid><description>介绍如何使用 DeepSeek-Commit-Tool 工具，通过 DeepSeek API 自动生成规范的 Git 提交信息，提升开发效率。</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;
import { Card } from &apos;astro-pure/user&apos;
import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在日常开发工作中，无论是 GitHub 还是公司内部的 Gerrit，我们都需要频繁提交代码。然而，每次填写 commit 信息时，总是难以用简洁明了的语言完整概括本次修改的内容。这导致提交信息要么过于简单，要么冗长且不够规范。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我开发了一个基于 DeepSeek API 的工具，能够自动分析代码变更并生成规范的 commit 信息，大大提升了提交效率。&lt;/p&gt;
&lt;h2&gt;下载&lt;/h2&gt;
&lt;h2&gt;API 配置&lt;/h2&gt;
&lt;h3&gt;环境变量配置&lt;/h3&gt;
&lt;p&gt;为了便于全局使用，建议将 &lt;code&gt;dsc.exe&lt;/code&gt; 配置到系统环境变量中。这样可以在任意目录下直接使用该工具。&lt;/p&gt;
&lt;h3&gt;获取 API Key&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://platform.deepseek.com/api_keys&quot; title=&quot;DeepSeek API Keys&quot;&gt;DeepSeek API 管理页面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;登录你的 DeepSeek 账户&lt;/li&gt;
&lt;li&gt;点击「创建新的 API Key」&lt;/li&gt;
&lt;li&gt;复制生成的 API Key（格式类似：sk-xxxxx）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置 API Key&lt;/h3&gt;
&lt;p&gt;通过以下命令配置你的 API Key：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dsc.exe --api-key sk-xxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置完成后，工具会将 API Key 保存到本地配置文件中，后续使用无需重复配置。&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;h3&gt;在 SourceTree 中集成&lt;/h3&gt;
&lt;p&gt;SourceTree 是一款流行的 Git 图形化客户端，下面介绍如何在该工具中集成 DeepSeek-Commit-Tool。&lt;/p&gt;
&lt;h4&gt;步骤一：配置自定义操作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;打开 SourceTree&lt;/li&gt;
&lt;li&gt;依次点击 工具 → 选项 → 自定义操作 → 添加&lt;/li&gt;
&lt;li&gt;按照以下配置填写：
&lt;ul&gt;
&lt;li&gt;菜单标题：&lt;code&gt;DeepSeek Commit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;运行的脚本：&lt;code&gt;dsc.exe&lt;/code&gt;（或完整路径）&lt;/li&gt;
&lt;li&gt;参数：&lt;code&gt;run $REPO&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击「确定」保存配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_251207_192555.png&quot; alt=&quot;配置自定义操作&quot;&gt;&lt;/p&gt;
&lt;h4&gt;步骤二：使用工具生成 Commit 信息&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;在项目中添加需要提交的文件到暂存区&lt;/li&gt;
&lt;li&gt;暂存区任意位置右键，选择自定义操作 → DeepSeek Commit&lt;/li&gt;
&lt;li&gt;工具会自动分析代码变更，并生成规范的 commit 信息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_251207_193156.png&quot; alt=&quot;执行操作&quot;&gt;&lt;/p&gt;
&lt;h3&gt;命令行使用&lt;/h3&gt;
&lt;p&gt;如果你习惯使用命令行，也可以直接在终端中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在 Git 仓库根目录下执行
dsc.exe run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工具会自动读取当前仓库的变更，生成 commit 信息并显示在终端中。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.C2XCaECb.jpg"/><enclosure url="/_astro/thumbnail.C2XCaECb.jpg"/></item><item><title>twikoo邮件通知模板</title><link>https://saneko.me/blog/84d81a30c231</link><guid isPermaLink="true">https://saneko.me/blog/84d81a30c231</guid><description>本文主要分享twikoo中邮件通知模板</description><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;写在前面&lt;/h3&gt;
&lt;p&gt;样式主要参考了&lt;a href=&quot;https://www.kzhik.cn/friends?atk_comment=57&amp;#x26;atk_notify_key=XVS5D&quot;&gt;kzhik&lt;/a&gt;的通知样式，很好看!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250913_224355.png&quot; alt=&quot;kzhik的通知样式&quot;&gt;&lt;/p&gt;
&lt;h3&gt;MAIL_TEMPLATE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div
    style=&quot;font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;&quot;&gt;
    &amp;#x3C;div
        style=&quot;border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; background-color: #ffffff;&quot;&gt;
        &amp;#x3C;!-- 头部区域 --&gt;
        &amp;#x3C;div
            style=&quot;background-color: #f8db8f; padding: 20px; color: white; display: flex; align-items: center; justify-content: center; gap: 20px;&quot;&gt;
            &amp;#x3C;div style=&quot;text-align: center;&quot;&gt;
                &amp;#x3C;div
                    style=&quot;font-size: 28px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 10px;&quot;&gt;
                    &amp;#x3C;span style=&quot;font-size: 26px;&quot;&gt;👋&amp;#x3C;/span&gt; Hi ${PARENT_NICK} &amp;#x3C;/div&gt;
                &amp;#x3C;div style=&quot;margin-top: 10px; font-size:16px;&quot;&gt; 您在 &amp;#x3C;a href=&quot;${SITE_URL}&quot;
                        style=&quot;color: white; text-decoration: underline; font-weight: 500;&quot;&gt;${SITE_NAME}&amp;#x3C;/a&gt; 中收到一条新回复！
                &amp;#x3C;/div&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 右侧图片 --&gt; &amp;#x3C;img src=&quot;https://cdn.blog.saneko.me/Web/web_250913_194213.png&quot;
                style=&quot;display:block; max-width:100px;&quot;&gt;
        &amp;#x3C;/div&gt; &amp;#x3C;!-- 内容区域 --&gt;
        &amp;#x3C;div style=&quot;padding: 25px 20px;&quot;&gt; &amp;#x3C;!-- 原评论部分 --&gt;
            &amp;#x3C;div style=&quot;margin-bottom: 20px;&quot;&gt;
                &amp;#x3C;div style=&quot;font-weight: bold; margin-bottom: 10px; color: #333; font-size: 16px;&quot;&gt;您发表的评论：&amp;#x3C;/div&gt;
                &amp;#x3C;div style=&quot;background-color: #f5f7fa; border-radius: 6px; padding: 15px; border: 1px solid #ebf0f5;&quot;&gt;
                    &amp;#x3C;div style=&quot;color: #f8db8f; font-weight: 500; margin-bottom: 8px;&quot;&gt;@${PARENT_NICK}:&amp;#x3C;/div&gt;
                    &amp;#x3C;div style=&quot;color: #333; line-height: 1.6;&quot;&gt;
                        &amp;#x3C;div style=&quot;margin-bottom: 5px;&quot;&gt;${PARENT_COMMENT}&amp;#x3C;/div&gt;
                    &amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 收到的回复部分 --&gt;
            &amp;#x3C;div style=&quot;margin-bottom: 30px;&quot;&gt;
                &amp;#x3C;div style=&quot;font-weight: bold; margin-bottom: 10px; color: #333; font-size: 16px;&quot;&gt;您收到的回复&amp;#x3C;/div&gt;
                &amp;#x3C;div style=&quot;background-color: #f5f7fa; border-radius: 6px; padding: 15px; border: 1px solid #ebf0f5;&quot;&gt;
                    &amp;#x3C;div style=&quot;color: #f8db8f; font-weight: 500; margin-bottom: 8px;&quot;&gt;@${NICK}:&amp;#x3C;/div&gt;
                    &amp;#x3C;div style=&quot;color: #333; line-height: 1.6;&quot;&gt;${COMMENT}&amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 回复按钮 --&gt;
            &amp;#x3C;div style=&quot;margin-bottom: 30px;&quot;&gt; &amp;#x3C;a href=&quot;${POST_URL}&quot;
                    style=&quot;display: inline-block; background-color: #f8db8f; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; font-weight: 500;&quot;&gt;回复&amp;#x3C;/a&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 系统提示 --&gt;
            &amp;#x3C;div
                style=&quot;color: #86909c; font-size: 12px; text-align: center; padding-top: 15px; border-top: 1px solid #ebf0f5;&quot;&gt;
                此邮件由系统发送，请不要回复此邮件 &amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式参考:
&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250913_224556.png&quot; alt=&quot;MAIL_TEMPLATE样式&quot;&gt;&lt;/p&gt;
&lt;h3&gt;MAIL_TEMPLATE_ADMIN&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div
    style=&quot;font-family: -apple-system, BlinkMacSystemFont, &apos;Segoe UI&apos;, Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;&quot;&gt;
    &amp;#x3C;div
        style=&quot;border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; background-color: #ffffff;&quot;&gt;
        &amp;#x3C;!-- 头部区域 --&gt;
        &amp;#x3C;div
            style=&quot;background-color: #f8db8f; padding: 20px; color: white; display: flex; align-items: center; justify-content: center; gap: 20px;&quot;&gt;
            &amp;#x3C;div style=&quot;text-align: center;&quot;&gt;
                &amp;#x3C;div
                    style=&quot;font-size: 28px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 10px;&quot;&gt;
                    &amp;#x3C;span style=&quot;font-size: 26px;&quot;&gt;👋&amp;#x3C;/span&gt; Hi ATao &amp;#x3C;/div&gt;
                &amp;#x3C;div style=&quot;margin-top: 10px; font-size:16px;&quot;&gt; 您在 &amp;#x3C;a href=&quot;${SITE_URL}&quot;
                        style=&quot;color: white; text-decoration: underline; font-weight: 500;&quot;&gt;${SITE_NAME}&amp;#x3C;/a&gt; 中收到一条新的评论！
                &amp;#x3C;/div&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 右侧图片 --&gt; &amp;#x3C;img src=&quot;https://cdn.blog.saneko.me/Web/web_250913_213324.png&quot;
                style=&quot;display:block; max-width:100px;&quot;&gt;
        &amp;#x3C;/div&gt; &amp;#x3C;!-- 内容区域 --&gt;
        &amp;#x3C;div style=&quot;padding: 25px 20px;&quot;&gt; &amp;#x3C;!-- 原评论部分 --&gt;
            &amp;#x3C;div style=&quot;margin-bottom: 20px;&quot;&gt;
                &amp;#x3C;div style=&quot;font-weight: bold; margin-bottom: 10px; color: #333; font-size: 16px;&quot;&gt;评论内容：&amp;#x3C;/div&gt;
                &amp;#x3C;div style=&quot;background-color: #f5f7fa; border-radius: 6px; padding: 15px; border: 1px solid #ebf0f5;&quot;&gt;
                    &amp;#x3C;div style=&quot;color: #f8db8f; font-weight: 500; margin-bottom: 8px;&quot;&gt;@${NICK}:&amp;#x3C;/div&gt;
                    &amp;#x3C;div style=&quot;color: #333; line-height: 1.6;&quot;&gt;
                        &amp;#x3C;div style=&quot;margin-bottom: 5px;&quot;&gt;${COMMENT}&amp;#x3C;/div&gt;
                    &amp;#x3C;/div&gt;
                &amp;#x3C;/div&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 回复按钮 --&gt;
            &amp;#x3C;div style=&quot;margin-bottom: 30px;&quot;&gt; &amp;#x3C;a href=&quot;${POST_URL}&quot;
                    style=&quot;display: inline-block; background-color: #f8db8f; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; font-weight: 500;&quot;&gt;回复&amp;#x3C;/a&gt;
            &amp;#x3C;/div&gt; &amp;#x3C;!-- 系统提示 --&gt;
            &amp;#x3C;div
                style=&quot;color: #86909c; font-size: 12px; text-align: center; padding-top: 15px; border-top: 1px solid #ebf0f5;&quot;&gt;
                此邮件由系统发送，请不要回复此邮件 &amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式参考:
&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250913_224612.png&quot; alt=&quot;MAIL_TEMPLATE样式&quot;&gt;&lt;/p&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;p&gt;一开始配置了&lt;code&gt;MAIL_TEMPLATE_ADMIN&lt;/code&gt;，但无论如何测试都无法收到邮件。由于我的Twikoo是通过Docker部署的，通过查看日志发现了原因：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;9/12/2025, 2:05:18 PM Twikoo: 存在即时消息推送配置，默认不发送邮件给博主，您可以在管理面板修改此行为
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来是因为我配置了&lt;code&gt;即时通知&lt;/code&gt;功能，导致Twikoo默认不发送邮件通知。取消该配置后，邮件通知功能就恢复正常了。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Dc-shwC3.jpg"/><enclosure url="/_astro/thumbnail.Dc-shwC3.jpg"/></item><item><title>企业微信每日提醒机器人</title><link>https://saneko.me/blog/2580bdab69aa</link><guid isPermaLink="true">https://saneko.me/blog/2580bdab69aa</guid><description>本文介绍如何使用企业微信提醒机器人，实现定时发送消息。</description><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;需求背景&lt;/h3&gt;
&lt;p&gt;需要定时发送消息到企业微信群，实现每日提醒功能。&lt;/p&gt;
&lt;h3&gt;实现方法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在企业微信中的群聊，添加机器人&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250909_110204.png&quot; alt=&quot;添加机器人&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;获取机器人的Webhook地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250909_110608.png&quot; alt=&quot;获取Webhook地址&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;
&lt;p&gt;打开并登录&lt;a href=&quot;https://ipaas.cloud.tencent.com/&quot;&gt;腾讯轻联&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左侧导航栏中，点击&lt;code&gt;新建流程&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;触发组件中选择&lt;code&gt;定时启动&lt;/code&gt;, 执行流程中添加&lt;code&gt;企业微信群机器人&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250909_111709.png&quot; alt=&quot;设置流程&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;
&lt;p&gt;设置定时启动的触发条件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置机器人发送的消息内容&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存并上线流程&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/thumbnail.JC8GnBuH.jpg"/><enclosure url="/_astro/thumbnail.JC8GnBuH.jpg"/></item><item><title>AI大模型常见术语</title><link>https://saneko.me/blog/73c23a2a50e9</link><guid isPermaLink="true">https://saneko.me/blog/73c23a2a50e9</guid><description>汇总了AI大模型中常见的名词术语及解释。</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;模型基础与结构&lt;/h2&gt;
&lt;h3&gt;大语言模型（LLM, Large Language Model）&lt;/h3&gt;
&lt;p&gt;基于海量文本训练的大规模语言模型，能理解和生成类人文本（如 GPT、LLaMA、Qwen）&lt;/p&gt;
&lt;h3&gt;Transformer&lt;/h3&gt;
&lt;p&gt;LLM 的核心架构，基于自注意力机制，支持并行处理序列数据。&lt;/p&gt;
&lt;h3&gt;自注意力机制&lt;/h3&gt;
&lt;p&gt;Transformer 的核心，让模型计算文本中每个 Token 与其他 Token 的关联（如 “他” 指代前文的 “小明”）&lt;/p&gt;
&lt;h3&gt;模型深度&lt;/h3&gt;
&lt;p&gt;Transformer 的层数（如 GPT-3 有 96 层），深度越深，模型捕捉复杂规律的能力越强。&lt;/p&gt;
&lt;h2&gt;参数与规模&lt;/h2&gt;
&lt;h3&gt;模型参数&lt;/h3&gt;
&lt;p&gt;模型训练中学习的权重和偏置（如 “7B 模型” 指 70 亿参数），参数量影响模型能力和资源需求。&lt;/p&gt;
&lt;h3&gt;参数量级&lt;/h3&gt;
&lt;p&gt;模型参数的规模等级（如 B = 十亿，13B 即 130 亿）。&lt;/p&gt;
&lt;h3&gt;上下文窗口&lt;/h3&gt;
&lt;p&gt;模型能处理的最大输入长度（以 Token 为单位），即 “上下文长度”。&lt;/p&gt;
&lt;h3&gt;Token&lt;/h3&gt;
&lt;p&gt;文本的基本处理单位（如中文单字、英文子词），模型输入输出均以 Token 为单位。&lt;/p&gt;
&lt;h3&gt;词汇表&lt;/h3&gt;
&lt;p&gt;模型可识别的所有 Token 集合（如 GPT-2 词汇表约 5 万 Token），未在表中的词会被拆分为子词。&lt;/p&gt;
&lt;h2&gt;训练与优化&lt;/h2&gt;
&lt;h3&gt;预训练&lt;/h3&gt;
&lt;p&gt;模型在海量通用文本（如书籍、网页）上的初始训练，学习语言规律和世界知识。&lt;/p&gt;
&lt;h3&gt;预训练数据&lt;/h3&gt;
&lt;p&gt;用于预训练的文本集合（如 GPT-3 用了约 45TB 文本），数据质量和多样性影响模型能力。&lt;/p&gt;
&lt;h3&gt;微调&lt;/h3&gt;
&lt;p&gt;预训练模型在特定任务数据（如医疗问答）上的二次训练，适配具体场景。&lt;/p&gt;
&lt;h3&gt;指令微调&lt;/h3&gt;
&lt;p&gt;用 “指令 - 响应” 格式数据微调模型，让模型理解人类指令（如 “写一篇总结”）。&lt;/p&gt;
&lt;h3&gt;过拟合&lt;/h3&gt;
&lt;p&gt;模型过度拟合训练数据，在新数据上表现差（如生成内容局限于训练文本）。&lt;/p&gt;
&lt;h3&gt;泛化能力&lt;/h3&gt;
&lt;p&gt;模型对未见过的新文本的适应能力，是评估 LLM 的核心指标。&lt;/p&gt;
&lt;h2&gt;数据表示与转换&lt;/h2&gt;
&lt;h3&gt;向量化&lt;/h3&gt;
&lt;p&gt;将文本、图像等非结构化数据转换为数值向量（Embedding）的过程。&lt;/p&gt;
&lt;h3&gt;嵌入向量&lt;/h3&gt;
&lt;p&gt;向量化的结果，是文本语义的数值表示。&lt;/p&gt;
&lt;h3&gt;向量数据库&lt;/h3&gt;
&lt;p&gt;专门存储和检索嵌入向量的数据库，通过高效的近似最近邻（ANN）算法，快速从海量向量中找到与查询向量最相似的结果，是 RAG 系统的 “知识库存储核心”。&lt;/p&gt;
&lt;h2&gt;模型压缩与优化&lt;/h2&gt;
&lt;h3&gt;量化&lt;/h3&gt;
&lt;p&gt;降低模型参数精度（如 FP16→INT8→INT4），减少显存占用和计算量（如 7B 模型从 14GB 降至 3.5GB）。&lt;/p&gt;
&lt;h3&gt;AWQ&lt;/h3&gt;
&lt;p&gt;一种高效量化方法，通过激活感知权重量化，在低精度下保留模型性能，适合 LLM 部署。&lt;/p&gt;
&lt;h3&gt;GPTQ&lt;/h3&gt;
&lt;p&gt;另一种常用量化方法，通过优化量化误差，支持 4bit 量化，广泛用于社区模型。&lt;/p&gt;
&lt;h3&gt;模型蒸馏&lt;/h3&gt;
&lt;p&gt;一种模型压缩技术：用性能强的 “教师模型”（如 100B 大模型）指导 “学生模型”（如 7B 小模型）学习，通过模仿教师模型的输出分布（而非仅学习标签），让小模型在体积更小、速度更快的同时，保留接近大模型的能力。&lt;/p&gt;
&lt;h3&gt;知识蒸馏&lt;/h3&gt;
&lt;p&gt;将教师模型学到的 “暗知识”（如对不同答案的概率分布）迁移给学生模型，而非仅传递 “明知识”（如正确答案），使小模型更懂 “推理逻辑”。&lt;/p&gt;
&lt;h3&gt;剪枝&lt;/h3&gt;
&lt;p&gt;直接删除模型中冗余的参数或结构（如权重接近 0 的神经元、贡献度低的网络层），减少模型体积和计算量。与蒸馏不同，剪枝是 “减法”（删参数），蒸馏是 “模仿学习”。&lt;/p&gt;
&lt;h2&gt;数值精度与计算&lt;/h2&gt;
&lt;h3&gt;FP32&lt;/h3&gt;
&lt;p&gt;32 位精度的浮点数，是传统深度学习的默认数据类型，精度最高但显存占用大（1 个 FP32 参数占 4 字节）。大模型训练初期可能用 FP32，但推理时极少使用（太耗资源）。&lt;/p&gt;
&lt;h3&gt;FP16&lt;/h3&gt;
&lt;p&gt;16 位精度的浮点数，显存占用仅为 FP32 的一半（1 个参数占 2 字节），计算速度更快，广泛用于模型推理和部分训练场景（如混合精度训练）。但精度较低，极端值可能溢出。&lt;/p&gt;
&lt;h3&gt;BF16&lt;/h3&gt;
&lt;p&gt;16 位精度的浮点数，由谷歌为 TPU 设计，指数位与 FP32 相同（保留大范围数值），尾数位更短（牺牲部分精度）。适合大模型训练（抗噪性强），与 FP16 相比，在极端值处理上更稳定，常见于 A100、H100 等高端 GPU。&lt;/p&gt;
&lt;h3&gt;INT8/INT4&lt;/h3&gt;
&lt;p&gt;整数精度数据类型，INT8 参数占 1 字节（为 FP32 的 1/4），INT4 仅占 0.5 字节（为 FP32 的 1/8），是量化技术的核心。通过降低精度大幅减少显存占用，适合低资源设备部署。&lt;/p&gt;
&lt;h3&gt;混合精度&lt;/h3&gt;
&lt;p&gt;训练 / 推理中同时使用多种精度，平衡精度与效率。&lt;/p&gt;
&lt;h2&gt;输入与输出处理&lt;/h2&gt;
&lt;h3&gt;分词器&lt;/h3&gt;
&lt;p&gt;将文本拆分为 Token 的工具，是 LLM 处理文本的第一步。&lt;/p&gt;
&lt;h3&gt;Prompt（提示词）&lt;/h3&gt;
&lt;p&gt;用户输入的指令或问题（如 “解释什么是 LLM”），用于引导模型生成输出。&lt;/p&gt;
&lt;h3&gt;提示词工程&lt;/h3&gt;
&lt;p&gt;设计优化 Prompt 的方法（如添加示例、限定格式），提升模型输出质量。&lt;/p&gt;
&lt;h3&gt;少样本提示&lt;/h3&gt;
&lt;p&gt;在 Prompt 中加入少量示例（如 “例 1：... 例 2：... 请解决：...”），帮助模型理解任务。&lt;/p&gt;
&lt;h3&gt;零样本提示&lt;/h3&gt;
&lt;p&gt;不提供示例，直接用指令让模型处理任务（如 “翻译‘你好’为英文”）。&lt;/p&gt;
&lt;h3&gt;输出长度&lt;/h3&gt;
&lt;p&gt;模型生成文本的最大 Token 数，可通过参数限制。&lt;/p&gt;
&lt;h3&gt;温度参数&lt;/h3&gt;
&lt;p&gt;控制输出随机性的参数（0 = 确定性输出，1 = 高随机性），值越低输出越固定。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;vLLM&lt;/h3&gt;
&lt;p&gt;高效的 LLM 推理框架，通过 PagedAttention 技术优化显存使用，提升吞吐量。&lt;/p&gt;
&lt;h3&gt;Tensor Parallelism（张量并行）&lt;/h3&gt;
&lt;p&gt;将模型参数拆分到多个 GPU 上，解决单卡显存不足问题（如 13B 模型拆到 2 张卡）。&lt;/p&gt;
&lt;h3&gt;Pipeline Parallelism（流水线并行）&lt;/h3&gt;
&lt;p&gt;将模型层拆分到多个 GPU，按顺序处理数据，提升大模型训练 / 推理效率。&lt;/p&gt;
&lt;h3&gt;KV 缓存（KV Cache）&lt;/h3&gt;
&lt;p&gt;缓存推理过程中的 Key 和 Value 向量（自注意力计算结果），避免重复计算，加速生成。&lt;/p&gt;
&lt;h2&gt;应用与扩展&lt;/h2&gt;
&lt;h3&gt;生成式 AI&lt;/h3&gt;
&lt;p&gt;能生成新内容的 AI，LLM 是其核心（如生成文章、代码、对话）。&lt;/p&gt;
&lt;h3&gt;文本生成&lt;/h3&gt;
&lt;p&gt;LLM 的核心能力，生成符合语境的文本（如写邮件、编故事）。&lt;/p&gt;
&lt;h3&gt;问答系统&lt;/h3&gt;
&lt;p&gt;LLM 根据问题生成答案（如 “地球半径是多少？”→输出具体数值）。&lt;/p&gt;
&lt;h3&gt;机器翻译&lt;/h3&gt;
&lt;p&gt;将一种语言转换为另一种（如 LLM 支持中英互译）。&lt;/p&gt;
&lt;h3&gt;摘要生成&lt;/h3&gt;
&lt;p&gt;将长文本压缩为简洁摘要（如把一篇论文缩为一段话）。&lt;/p&gt;
&lt;h3&gt;代码生成&lt;/h3&gt;
&lt;p&gt;LLM 根据指令生成代码（如 “写一个 Python 排序函数”）。&lt;/p&gt;
&lt;h3&gt;RAG（检索增强生成）&lt;/h3&gt;
&lt;p&gt;结合外部知识库检索与 LLM 生成，让输出引用真实信息（如企业知识库问答）。&lt;/p&gt;
&lt;h3&gt;多轮对话&lt;/h3&gt;
&lt;p&gt;LLM 记住对话历史，支持连续交互（如聊天机器人）。&lt;/p&gt;
&lt;h3&gt;工具调用&lt;/h3&gt;
&lt;p&gt;LLM 调用外部工具（如计算器、搜索引擎）完成任务（如 “查今天的天气”）。&lt;/p&gt;
&lt;h3&gt;多模态&lt;/h3&gt;
&lt;p&gt;LLM 结合图像、音频等数据（如 GPT-4V 能理解图片 + 文本）。&lt;/p&gt;
&lt;h2&gt;模型类型与特性&lt;/h2&gt;
&lt;h3&gt;基座模型&lt;/h3&gt;
&lt;p&gt;仅经过预训练的模型，需微调后才能用于具体任务。&lt;/p&gt;
&lt;h3&gt;对话模型&lt;/h3&gt;
&lt;p&gt;经指令微调 + RLHF 优化的模型，适合直接对话。&lt;/p&gt;
&lt;h3&gt;开源模型&lt;/h3&gt;
&lt;p&gt;公开模型权重和代码，允许自由使用和修改（如 LLaMA2、Qwen）。&lt;/p&gt;
&lt;h3&gt;闭源模型&lt;/h3&gt;
&lt;p&gt;不公开权重，仅通过 API 提供服务（如 GPT-4、Claude）。&lt;/p&gt;
&lt;h3&gt;MoE（混合专家模型）&lt;/h3&gt;
&lt;p&gt;模型包含多个 “专家子网络”，输入由路由层分配给部分专家处理，平衡规模与效率（如 GLaM）。&lt;/p&gt;
&lt;h3&gt;长上下文模型&lt;/h3&gt;
&lt;p&gt;支持超长输入的 LLM（如 Qwen3 支持 128k Token，可处理整本书）。&lt;/p&gt;
&lt;h2&gt;评估与问题&lt;/h2&gt;
&lt;h3&gt;困惑度（PPL）&lt;/h3&gt;
&lt;p&gt;评估模型对文本的预测能力，值越低表示模型越 “理解” 文本（如 PPL=10 优于 PPL=20）。&lt;/p&gt;
&lt;h3&gt;BLEU 分数&lt;/h3&gt;
&lt;p&gt;评估机器翻译质量的指标，衡量输出与参考译文的重叠度。&lt;/p&gt;
&lt;h3&gt;ROUGE 分数&lt;/h3&gt;
&lt;p&gt;评估文本摘要质量的指标，对比摘要与原文的相似度。&lt;/p&gt;
&lt;h3&gt;幻觉&lt;/h3&gt;
&lt;p&gt;LLM 生成错误或不存在的信息（如编造事实、引用假数据）。&lt;/p&gt;
&lt;h3&gt;对齐&lt;/h3&gt;
&lt;p&gt;让 LLM 输出符合人类价值观和需求（如避免有害内容、遵循伦理）。&lt;/p&gt;
&lt;h3&gt;偏见&lt;/h3&gt;
&lt;p&gt;LLM 因训练数据包含偏见（如性别、种族偏见），输出带有倾向性内容。&lt;/p&gt;
&lt;h3&gt;鲁棒性&lt;/h3&gt;
&lt;p&gt;LLM 对输入微小变化（如错别字）的稳定性，鲁棒性差易输出错误。&lt;/p&gt;
&lt;h2&gt;工具与生态&lt;/h2&gt;
&lt;h3&gt;Hugging Face&lt;/h3&gt;
&lt;p&gt;开源 AI 社区，提供 LLM 模型、工具库（如 Transformers）和部署平台。&lt;/p&gt;
&lt;h3&gt;Transformers 库&lt;/h3&gt;
&lt;p&gt;Hugging Face 推出的工具库，简化 LLM 加载、训练和推理（支持多框架）。&lt;/p&gt;
&lt;h3&gt;LangChain&lt;/h3&gt;
&lt;p&gt;构建 LLM 应用的框架，支持 RAG、工具调用、多轮对话等复杂流程。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.B0UrFroS.jpg"/><enclosure url="/_astro/thumbnail.B0UrFroS.jpg"/></item><item><title>本地部署vllm</title><link>https://saneko.me/blog/fd690b535b97</link><guid isPermaLink="true">https://saneko.me/blog/fd690b535b97</guid><description>使用Docker部署vLLM大语言模型推理服务。</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;vLLM&lt;/strong&gt; 是一个专为大规模语言模型（LLM）推理和服务而设计的高性能库。&lt;/p&gt;
&lt;p&gt;它的核心优势在于其创新的 PagedAttention 算法，该算法解决了传统服务方式中内存管理的瓶颈，从而带来了两大核心优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;极高的吞吐量&lt;/code&gt;：在相同硬件条件下，vLLM 可以同时处理更多用户的请求，显著降低了服务成本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;高效的内存利用&lt;/code&gt;：极大地减少了模型运行所需的显存，使得在消费级GPU上运行大模型成为可能，或者可以在单卡上运行更大的模型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单来说，如果你需要将像 Deepseek、Qwen3 等这类大模型部署成可对外服务的 API，vLLM 通常是性能和易用性的最佳选择之一。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;使用 Docker 部署是最简单、最干净的方式，它能避免复杂的环境依赖问题。&lt;/p&gt;
&lt;h3&gt;前提条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;确保你的机器已安装 &lt;code&gt;Docker&lt;/code&gt; 和 &lt;code&gt;NVIDIA Docker Toolkit&lt;/code&gt;（如果你使用 NVIDIA GPU）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拥有足够的 GPU 显存（例如，运行 7B 模型建议至少 16GB 显存）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;预先下载模型到宿主机&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先，在你的服务器或本地机器上，创建一个专门的目录来存放模型，然后使用 huggingface-hub 官方工具下载模型。&lt;a href=&quot;https://huggingface.co/&quot;&gt;huggingface&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 安装模型下载工具
pip install huggingface-hub

# 2. 创建模型存储目录（建议选择一个空间大的磁盘）
mkdir -p /data/models

# 3. 下载你所需的模型（这里以 Qwen 为例）
huggingface-cli download Qwen/Qwen3-8B-AWQ --local-dir /data/models/Qwen3-8B-AWQ

# 或者也可以通过git进行拉取
# git clone https://huggingface.co/Qwen/Qwen3-8B-AWQ
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;编写 Docker Compose 配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;创建一個名为 docker-compose.yml 的文件，内容如下。这个配置几乎可以直接使用，你只需要修改 volumes 项中的模型路径即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;version: &apos;3.8&apos;

services:
  vllm-openai:
    image: vllm/vllm-openai:latest
    container_name: Qwen3-8B-AWQ # 给你的容器起个名字
    ports:
      - &apos;8000:8000&apos; # 将宿主机的8000端口映射到容器内
    volumes:
      # 关键步骤：将宿主机上下载好的模型目录挂载到容器内
      - /data/models/Qwen3-8B:/app/model
    environment:
      - LC_ALL=C.UTF-8 # 设置容器内部语言环境，避免编码问题
      - HF_HUB_OFFLINE=1 # 强制使用离线模式，避免运行时检查网络
      - TRANSFORMERS_OFFLINE=1 # 强制Transformers库使用离线模式
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all # 使用所有可用的GPU
              capabilities: [gpu] # 声明需要NVIDIA GPU能力
    # 传递给vLLM服务的启动命令,参数分别为
    # --model 指定容器内的模型路径
    # --gpu-memory-utilizatio GPU内存使用率，根据需求调整
    # --dtype 选择数据类型
    # --tensor-parallel-size tensor并行数
    # --port 容器内服务监听端口
    # --api-key 密钥
    # --trust-remote-code 对于Qwen等模型，需要此参数
    command: &gt;
      --model /app/model
      --gpu-memory-utilization 0.9
      --dtype auto
      --tensor-parallel-size 1
      --port 8000
      --api-key 123123
      --trust-remote-code
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;启动服务
在包含 docker-compose.yml 文件的目录下，执行以下命令即可启动所有服务。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d # -d 参数代表在后台运行
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;验证服务
服务启动后，使用 curl 命令测试接口是否正常工作。使用在 --api-key 参数中设置的密钥。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -H &quot;Authorization: Bearer 123123&quot; http://localhost:8000/v1/models
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果返回了包含模型信息的 JSON 数据，恭喜，部署成功！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{

  &quot;object&quot;: &quot;list&quot;,
  &quot;data&quot;: [{
    &quot;id&quot;: &quot;/app/model&quot;,
    &quot;object&quot;: &quot;model&quot;,
    &quot;created&quot;: 1756171599,
    &quot;owned_by&quot;: &quot;vllm&quot;,
    &quot;root&quot;: &quot;/app/model&quot;,
    &quot;parent&quot;: null,
    &quot;max_model_len&quot;: 40960,
    &quot;permission&quot;: [{
      &quot;id&quot;: &quot;modelperm-e234d316935c4aae9fbe1b5cefe3e96c&quot;,
      &quot;object&quot;: &quot;model_permission&quot;,
      &quot;created&quot;: 1756171599,
      &quot;allow_create_engine&quot;: false,
      &quot;allow_sampling&quot;: true,
      &quot;allow_logprobs&quot;: true,
      &quot;allow_search_indices&quot;: false,
      &quot;allow_view&quot;: true,
      &quot;allow_fine_tuning&quot;: false,
      &quot;organization&quot;: &quot;*&quot;,
      &quot;group&quot;: null,
      &quot;is_blocking&quot;: false,
    },],
  },],
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.XadTrP9R.jpg"/><enclosure url="/_astro/thumbnail.XadTrP9R.jpg"/></item><item><title>一种轻APP的通知推送方案</title><link>https://saneko.me/blog/013dc8b78615</link><guid isPermaLink="true">https://saneko.me/blog/013dc8b78615</guid><description>本文介绍一种轻量、可自托管的APP通知推送方案PushDeer。</description><pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;前提背景&lt;/h2&gt;
&lt;p&gt;在日常工作和生活中，我们经常需要将&lt;strong&gt;设备状态&lt;/strong&gt;、&lt;strong&gt;程序日志&lt;/strong&gt;、&lt;strong&gt;重要提醒&lt;/strong&gt;等信息及时推送到手机或其他终端。&lt;/p&gt;
&lt;p&gt;但传统的推送方案要么依赖第三方闭源服务（存在隐私和稳定性风险），要么需要安装专用 APP（增加用户操作成本）。&lt;/p&gt;
&lt;p&gt;今天要介绍的&lt;code&gt;PushDeer&lt;/code&gt;，正是一款以 &lt;code&gt;“轻量、可控、易用” &lt;/code&gt;为核心的通知推送工具，尤其适合需要自主掌控推送链路的用户。&lt;/p&gt;
&lt;h2&gt;PushDeer&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;PushDeer&lt;/code&gt; 是一款开源的无 APP 推送服务，同时支持轻 APP（如 APP Clip、快应用）、传统 APP（iOS/Android/Mac）以及自制硬件设备（如 ESP8266/ESP32），核心目标是让消息推送 “无需安装、简单调用、自主可控”。&lt;/p&gt;
&lt;p&gt;其核心价值可以概括为三点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;易用性&lt;/code&gt; 无需安装 APP，通过轻应用或快应用即可接收消息；调用方式极简，只需一个 URL 即可发送文本，无需深入阅读文档。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;可控性&lt;/code&gt; 支持自托管，避免依赖第三方服务下线风险；非商用场景免费，且不依赖微信等平台的消息接口，减少政策限制影响。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;渐进性&lt;/code&gt; 基础功能（文本推送）零门槛，通过扩展参数可支持 Markdown、图片等富文本；后期可通过 APP 补充轻应用无法覆盖的功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自定义部署&lt;/h2&gt;
&lt;p&gt;如果需要完全掌控推送服务（避免依赖官方服务），可以通过以下步骤自托管 PushDeer。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;环境准备&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器：推荐 Linux 系统（需支持 Docker），具备公网 IP（方便外部调用）。&lt;/li&gt;
&lt;li&gt;本地工具：安装git、docker和docker-compose。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载代码
克隆官方仓库到服务器&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/easychen/pushdeer.git
cd pushdeer
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;启动服务
执行以下命令启动 API 服务&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose -f docker-compose.self-hosted.yml up --build -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，访问http://公网ip:8800，可以正常显示下方二维码则表示部署成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250821_141617.png&quot; alt=&quot;访问8800端口&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;客户端配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在苹果商店搜索「PushDeer·自架版」或扫描上方的二维码进行安装并启动&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250821_143221.jpg&quot; alt=&quot;登录apk&quot;&gt;&lt;/p&gt;
&lt;p&gt;API服务地址输入http://公网ip:8800, 并用苹果id进行登录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250821_143435.jpg&quot; alt=&quot;注册&quot;&gt;&lt;/p&gt;
&lt;p&gt;登陆后，没有注册就先进行注册&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250821_143528.jpg&quot; alt=&quot;注册完成&quot;&gt;&lt;/p&gt;
&lt;p&gt;注册完成后，点击&lt;code&gt;Key&lt;/code&gt;就可以看到当前设备的密钥了&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;验证服务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用python发送请求&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import requests
from typing import Optional

def pushdeer_send(text: str, desp: Optional[str] = None, type_: str = &quot;text&quot;, key: str = &quot;[PUSHKEY]&quot;) -&gt; str:
    &quot;&quot;&quot;
    发送 PushDeer 消息
    Parameters
    ----------
    text : str
        主体文字
    desp : Optional[str]
        附加描述，若为 None 则不传递此字段
    type_ : str
        消息类型，默认 &quot;text&quot;
    key  : str
        PushKey， 设备密钥

    Returns
    -------
    str
        PushDeer API 返回的原始文本内容。
    &quot;&quot;&quot;
    url = &quot;http://公网ip:8800/message/push&quot;

    payload = {
        &quot;text&quot;: text,
        &quot;type&quot;: type_,
        &quot;pushkey&quot;: key
    }
    if desp is not None:
        payload[&quot;desp&quot;] = desp

    response = requests.post(url, data=payload)

    response.raise_for_status()
    return response.text

pushdeer_send(&quot;Hello World&quot;,key=&quot;PDU1Tn3kcl...&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端接收到消息推送&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250821_143727.jpg&quot; alt=&quot;消息推送&quot;&gt;&lt;/p&gt;
&lt;h2&gt;相关问题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;容器启动失败，抛出以下报错&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-&gt; Executing /opt/docker/provision/entrypoint.d/05-permissions.sh
-&gt; Executing /opt/docker/provision/entrypoint.d/20-apache.sh
-&gt; Executing /opt/docker/provision/entrypoint.d/20-php-fpm.sh
-&gt; Executing /opt/docker/provision/entrypoint.d/20-php.sh
-&gt; Executing /opt/docker/provision/entrypoint.d/init.sh
/opt/docker/provision/entrypoint.d/init.sh: line 2: $$&apos;\r&apos;: command not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是由于脚本文件保存时使用了 Windows 换行符(CRLF), 而不是Linux 下的换行符(LF)&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://taonotespace.com/download/init.sh&quot;&gt;下载文件&lt;/a&gt;, 将文件拷贝到服务器的pushdeer目录下。
修改docker-compose.self-hosted.yml配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;# 其余的不变
app:
  image: pushdeer-app
  ports:
    - &apos;8800:80&apos;
  volumes:
    - &apos;./:/app&apos;
    - &apos;./init.sh:/opt/docker/provision/entrypoint.d/init.sh:ro&apos; # 挂载本地的init.sh文件
  depends_on:
    mariadb:
      condition: service_healthy
    redis:
      condition: service_healthy
  environment:
    - DB_HOST=mariadb
    - DB_PORT=3306
    - DB_USERNAME=root
    - DB_DATABASE=pushdeer
    - DB_PASSWORD=theVeryp@ssw0rd
    - GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
    - GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
    - APP_DEBUG=false
    - MQTT_API_KEY=9LKo3
    - MQTT_ON=false
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.BHf8y4PT.jpg"/><enclosure url="/_astro/thumbnail.BHf8y4PT.jpg"/></item><item><title>Ollama突然无法识别GPU问题</title><link>https://saneko.me/blog/453fb592d1ea</link><guid isPermaLink="true">https://saneko.me/blog/453fb592d1ea</guid><description>本文介绍Ollama在Docker中无法识别GPU的原因及解决方法。</description><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;在 Docker 容器中运行 &lt;code&gt;Ollama&lt;/code&gt; 时, GPU 最初可用，但运行一段时间后会突然失效，无法被 &lt;code&gt;Ollama&lt;/code&gt; 识别到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250805_094501.png&quot; alt=&quot;Ollama日志&quot;&gt;&lt;/p&gt;
&lt;h2&gt;原因分析&lt;/h2&gt;
&lt;p&gt;Docker 默认使用 &lt;code&gt;systemd&lt;/code&gt; 作为 cgroup 驱动（native.cgroupdriver=systemd），而 NVIDIA 容器运行时（nvidia-container-runtime）在某些情况下与 systemd 不兼容，导致:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GPU 设备权限丢失&lt;/strong&gt;：/dev/nvidia* 设备节点在运行过程中被 systemd 动态调整，导致容器无法访问&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GPU 无法识别&lt;/strong&gt;：nvidia-container-runtime 无法正确挂载 GPU 设备&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;修改 Docker 配置，使用 cgroupfs
编辑 &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;（如果不存在则新建）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;exec-opts&quot;: [&quot;native.cgroupdriver=cgroupfs&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 Docker 生效&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证是否生效&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker info | grep &quot;Cgroup Driver&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果输出是 &lt;code&gt;Cgroup Driver: cgroupfs&lt;/code&gt;，说明修改成功。&lt;/p&gt;
&lt;h2&gt;为什么这样修改有效&lt;/h2&gt;
&lt;p&gt;| &lt;strong&gt;&lt;code&gt;systemd&lt;/code&gt;（默认）&lt;/strong&gt;                  | &lt;strong&gt;&lt;code&gt;cgroupfs&lt;/code&gt;（推荐）&lt;/strong&gt;          |
| :------------------------------------- | :------------------------------ |
| 动态调整 cgroup，可能导致 GPU 设备丢失 | 直接管理 cgroup，GPU 访问更稳定 |
| 某些 NVIDIA 容器运行时兼容性较差       | 兼容性更好，适合 GPU 容器化     |
| 在长期运行的容器中可能出现问题         | 减少 GPU 失效的概率             |&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Fb2aq_Kz.jpg"/><enclosure url="/_astro/thumbnail.Fb2aq_Kz.jpg"/></item><item><title>常用Git指令</title><link>https://saneko.me/blog/c37071ba3194</link><guid isPermaLink="true">https://saneko.me/blog/c37071ba3194</guid><description>本文整理了常用Git指令及操作技巧。</description><pubDate>Mon, 21 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、Git基础配置&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 设置全局用户名和邮箱（每次提交都会记录这些信息）
git config --global user.name &quot;你的名字&quot;
git config --global user.email &quot;你的邮箱@example.com&quot;

# 查看所有配置信息
git config --list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些配置通常只需要设置一次，它们会被保存在用户目录下的.gitconfig文件中&lt;/p&gt;
&lt;h2&gt;二、仓库操作基础&lt;/h2&gt;
&lt;h3&gt;1. 创建与克隆仓库&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在当前目录初始化一个新的Git仓库
git init

# 克隆远程仓库到本地
git clone &amp;#x3C;仓库URL&gt;

# 克隆特定分支
git clone -b &amp;#x3C;分支名&gt; &amp;#x3C;仓库URL&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 远程仓库管理&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看已配置的远程仓库
git remote -v

# 添加新的远程仓库
git remote add &amp;#x3C;远程名称&gt; &amp;#x3C;仓库URL&gt;

# 修改远程仓库地址
git remote set-url &amp;#x3C;远程名称&gt; &amp;#x3C;新URL&gt;

# 删除远程仓库
git remote remove &amp;#x3C;远程名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;origin&lt;/code&gt;一般是在克隆(Clone)仓库时自动创建的默认远程仓库别名&lt;/p&gt;
&lt;h2&gt;三、日常开发工作流&lt;/h2&gt;
&lt;h3&gt;1. 状态查看与文件操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看当前仓库状态（推荐频繁使用）
git status

# 查看工作区和暂存区的具体更改
git diff

# 查看已暂存文件的更改
git diff --cached
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 提交更改&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 添加文件到暂存区
git add &amp;#x3C;文件名&gt;       # 添加特定文件
git add .              # 添加所有更改的文件

# 提交更改并添加描述信息
git commit -m &quot;有意义的提交信息&quot;

# 修改最后一次提交（未推送前）
git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见 Git Commit 关键字及含义
| 关键字    | 用途示例                     | 说明                                                                 |
|-----------|-----------------------------|----------------------------------------------------------------------|
| &lt;code&gt;feat&lt;/code&gt;    | feat: add user login      | 新增功能（Feature），通常对应 semver 的 MINOR 版本更新。              |
| &lt;code&gt;fix&lt;/code&gt;     | fix: button click bug     | 修复 Bug，通常对应 semver 的 PATCH 版本更新。                        |
| &lt;code&gt;docs&lt;/code&gt;    | docs: update README       | 文档更新（如 README、注释等）。                                      |
| &lt;code&gt;style&lt;/code&gt;   | style: format code        | 代码样式调整（如空格、缩进、分号等），不改变逻辑。                    |
| &lt;code&gt;refactor&lt;/code&gt;| refactor: simplify logic  | 代码重构（既非新增功能，也非修复 Bug）。                              |
| &lt;code&gt;perf&lt;/code&gt;    | perf: optimize rendering  | 性能优化（Performance）。                                            |
| &lt;code&gt;test&lt;/code&gt;    | test: add unit test       | 测试相关（新增或修改测试代码）。                                      |
| &lt;code&gt;chore&lt;/code&gt;   | chore: update deps        | 杂项任务（如构建配置、依赖更新等）。                                  |
| &lt;code&gt;build&lt;/code&gt;   | build: upgrade webpack    | 构建系统或工具链变更（如 Webpack、Babel 等）。                        |
| &lt;code&gt;ci&lt;/code&gt;      | ci: fix GitHub Actions    | CI/CD 配置变更（如 GitHub Actions、Jenkins）。                        |
| &lt;code&gt;revert&lt;/code&gt;  | revert: remove feature X  | 回滚之前的提交。                                                     |
| &lt;code&gt;merge&lt;/code&gt;   | merge: branch A into main | 合并分支（通常由 Git 自动生成，手动提交时应避免使用）。                |&lt;/p&gt;
&lt;h3&gt;3. 分支管理&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看本地分支
git branch

# 查看所有分支（包括远程）
git branch -a

# 创建新分支
git branch &amp;#x3C;分支名&gt;

# 切换分支
git checkout &amp;#x3C;分支名&gt;

# 创建并切换到新分支（推荐方式）
git checkout -b &amp;#x3C;分支名&gt;

# 删除分支
git branch -d &amp;#x3C;分支名&gt;      # 安全删除（已合并的分支）
git branch -D &amp;#x3C;分支名&gt;      # 强制删除（未合并的分支）

# 重命名分支
git branch -m &amp;#x3C;旧分支名&gt; &amp;#x3C;新分支名&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、同步与协作&lt;/h2&gt;
&lt;h3&gt;1. 拉取与推送&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 拉取远程仓库的最新更改（只获取不合并）
git fetch origin

# 推送当前分支到远程的Gerrit代码审查系统
git push origin HEAD:refs/for/&amp;#x3C;分支名&gt;

# 推送当前提交作为标签到远程仓库
git push origin HEAD:refs/tags/&amp;#x3C;标签名&gt;

# 标准推送方式
git push

# 推送新分支到远程
git push -u origin &amp;#x3C;分支名&gt;

# 强制推送（慎用，可能覆盖他人工作）
git push -f

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;强制推送会覆盖远程历史，只应在你完全确定的情况下使用，特别是在团队协作中要避免随意使用&lt;/p&gt;
&lt;h4&gt;2. 合并与变基&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 合并指定分支到当前分支
git merge &amp;#x3C;分支名&gt;

# 变基当前分支到指定分支（使历史更线性）
git rebase &amp;#x3C;分支名&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;合并(merge)会创建一个新的合并提交，保留完整的历史&lt;/li&gt;
&lt;li&gt;变基(rebase)会重写提交历史，使分支看起来像是直接基于目标分支开发的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;五、查看历史记录&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看完整的提交历史
git log

# 简洁版提交历史
git log --oneline

# 查看特定文件的修改历史
git blame &amp;#x3C;文件名&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;六、标签管理&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建轻量级标签
git tag &amp;#x3C;标签名&gt;

# 创建带注释的标签
git tag -a &amp;#x3C;标签名&gt; -m &quot;标签说明&quot;

# 查看所有标签
git tag

# 推送标签到远程
git push origin --tags

# 删除本地标签
git tag -d &amp;#x3C;标签名&gt;

# 删除远程标签
git push origin :refs/tags/&amp;#x3C;标签名&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;七、常用小技巧&lt;/h2&gt;
&lt;h3&gt;1. 强制重置到远程分支状态&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch origin
git reset --hard origin/&amp;#x3C;分支名&gt;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.BDNCu_zp.jpg"/><enclosure url="/_astro/thumbnail.BDNCu_zp.jpg"/></item><item><title>WPF实现本地化及运行时切换语言</title><link>https://saneko.me/blog/ab8de32d8402</link><guid isPermaLink="true">https://saneko.me/blog/ab8de32d8402</guid><description>本文介绍如何在WPF中通过资源字典实现本地化，并支持运行时切换语言。</description><pubDate>Wed, 18 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;添加资源文件&lt;/h3&gt;
&lt;p&gt;找一个存放本地化资源文件的目录，例如&lt;code&gt;/Resources/Languages&lt;/code&gt;目录下.&lt;/p&gt;
&lt;p&gt;右键添加&lt;code&gt;资源字典&lt;/code&gt;，这里简单命名为&lt;code&gt;Strings.zh-CN.xaml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;添加需要翻译的字段&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;ResourceDictionary
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:system=&quot;clr-namespace:System;assembly=mscorlib&quot;&gt;
    &amp;#x3C;!--  导航页面  --&gt;
    &amp;#x3C;system:String x:Key=&quot;Navigation_Home&quot;&gt;主页&amp;#x3C;/system:String&gt;
&amp;#x3C;/ResourceDictionary&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再新建另一种语言的资源字典，例如&lt;code&gt;Strings.en-US.xaml&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;ResourceDictionary
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:system=&quot;clr-namespace:System;assembly=mscorlib&quot;&gt;
    &amp;#x3C;!-- Navigation --&gt;
    &amp;#x3C;system:String x:Key=&quot;Navigation_Home&quot;&gt;Home&amp;#x3C;/system:String&gt;
&amp;#x3C;/ResourceDictionary&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;本地化管理&lt;/h3&gt;
&lt;p&gt;新建一个类&lt;code&gt;LanguageService&lt;/code&gt;，用于管理本地化语言。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public static class LanguageService
{
    private static ResourceDictionary _currentDictionary;

    public static void ChangeLanguage(string languageCode)
    {
        // 构建资源路径 (根据实际程序集名调整)
        string path = $&quot;pack://application:,,,/TWPFX_Gallery;component/Resources/Languages/Strings.{languageCode}.xaml&quot;;

        // 移除旧语言资源
        if (_currentDictionary != null)
        {
            Application.Current.Resources.MergedDictionaries.Remove(_currentDictionary);
        }

        // 加载新语言资源
        _currentDictionary = new ResourceDictionary { Source = new Uri(path) };
        Application.Current.Resources.MergedDictionaries.Add(_currentDictionary);
    }

    // 可选：自动检测系统语言
    public static void Initialize()
    {
        string sysLang = CultureInfo.CurrentCulture.Name;
        ChangeLanguage(sysLang == &quot;zh-CN&quot; ? &quot;zh-CN&quot; : &quot;en-US&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;XAML中使用&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;DynamicResource&lt;/code&gt;才可以支持运行时动态更新&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;ui:Button Content=&quot;{DynamicResource Navigation_Home}&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C#中使用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;button.Content = new DynamicResourceExtension(&quot;Navigation_Home&quot;).ProvideValue(null);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;切换语言&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;LanguageService.ChangeLanguage(&quot;en-US&quot;);
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.K9Qzcl6W.jpg"/><enclosure url="/_astro/thumbnail.K9Qzcl6W.jpg"/></item><item><title>Maxkb实现数据库备份与恢复</title><link>https://saneko.me/blog/baeb1f618a79</link><guid isPermaLink="true">https://saneko.me/blog/baeb1f618a79</guid><description>本文介绍Maxkb数据库的备份与恢复方法。</description><pubDate>Fri, 06 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;备份脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

# 定义变量
BACKUP_DIR=&quot;/opt/db_bak&quot;        # 备份文件存储目录
DATE=$(date +%Y%m%d_%H%M%S)     # 创建时间戳
CURRENT_TIME=$(date +&quot;%Y-%m-%d %H:%M:%S&quot;)  # 添加当前时间变量（用于日志）
DB_NAME=&quot;maxkb&quot;                 # 数据库名
DB_USER=&quot;postgres&quot;              # 数据库用户（根据配置修改）
DB_PASS=&quot;123123&quot;                # 数据库密码（根据配置修改）
DB_HOST=&quot;localhost&quot;             # 数据库主机地址
DB_PORT=&quot;5432&quot;                  # 数据库端口

# 创建备份文件名
BACKUP_FILE=&quot;$BACKUP_DIR/$DB_NAME-$DATE.dump&quot;

# 检查备份目录是否存在，如果不存在则创建
if [ ! -d &quot;$BACKUP_DIR&quot; ]; then
    mkdir -p &quot;$BACKUP_DIR&quot;
    echo &quot;[${CURRENT_TIME}] 备份目录不存在，已创建目录：$BACKUP_DIR&quot;
fi

# 执行备份
echo &quot;[${CURRENT_TIME}] 开始备份数据库：$DB_NAME&quot;

# 使用环境变量设置密码，避免在命令行中明文传递
PGPASSWORD=&quot;$DB_PASS&quot; pg_dump -h &quot;$DB_HOST&quot; -p &quot;$DB_PORT&quot; -U &quot;$DB_USER&quot; -d &quot;$DB_NAME&quot; -Fc &gt; &quot;$BACKUP_FILE&quot;

# 检查备份是否成功
if [ $? -eq 0 ]; then
    echo &quot;[${CURRENT_TIME}] 备份完成，文件路径：$BACKUP_FILE&quot;
else
    echo &quot;[${CURRENT_TIME}] 备份失败！&quot;
    exit 1
fi

# 压缩备份文件
gzip &quot;$BACKUP_FILE&quot;
COMPRESSED_FILE=&quot;${BACKUP_FILE}.gz&quot;
echo &quot;[${CURRENT_TIME}] 备份文件已压缩：$COMPRESSED_FILE&quot;
echo &quot;------------------------&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加执行权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo chmod +x /opt/maxkb/backup_maxkb.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo sh backup_maxkb.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时默认将数据库导入到&lt;code&gt;/opt/db_bak&lt;/code&gt;路径下，我们需要定时执行这个脚本。&lt;/p&gt;
&lt;p&gt;设置定时任务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件末尾添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 每周六凌晨2点进行备份
00 02 * * 6 /opt/backup_maxkb.sh &gt;&gt; /var/log/maxkb_backup.log 2&gt;&amp;#x26;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;恢复脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

# 定义变量
RESTORE_FILE=&quot;/opt/db_bak/maxkb-20250606_095233.dump.gz&quot;  # 备份文件路径，支持 .gz 压缩格式
DB_NAME=&quot;maxkb&quot;                                   # 要恢复的数据库名
DB_USER=&quot;postgres&quot;                                # 数据库用户（根据实际情况修改）
DB_PASS=&quot;123123&quot;                                  # 数据库密码（根据实际情况修改）
DB_HOST=&quot;localhost&quot;                               # 数据库主机地址
DB_PORT=&quot;5432&quot;                                    # 数据库端口
TEMP_DIR=&quot;/tmp/postgres_restore&quot;                 # 临时解压目录

# 创建临时目录
mkdir -p &quot;$TEMP_DIR&quot;
echo &quot;临时目录已创建：$TEMP_DIR&quot;

# 检查备份文件是否存在
if [ ! -f &quot;$RESTORE_FILE&quot; ]; then
    echo &quot;错误：恢复文件不存在：$RESTORE_FILE&quot;
    exit 1
fi

# 提取文件名（不含路径）
FILENAME=$(basename &quot;$RESTORE_FILE&quot;)

# 检查文件是否为 .gz 压缩格式
if [[ &quot;$FILENAME&quot; == *.gz ]]; then
    echo &quot;检测到压缩文件，开始解压...&quot;
    UNZIPPED_FILE=&quot;$TEMP_DIR/${FILENAME%.gz}&quot;  # 移除 .gz 后缀

    # 解压文件
    gunzip -c &quot;$RESTORE_FILE&quot; &gt; &quot;$UNZIPPED_FILE&quot;
    if [ $? -ne 0 ]; then
        echo &quot;错误：解压文件失败！&quot;
        exit 1
    fi
    echo &quot;文件已解压至：$UNZIPPED_FILE&quot;
else
    # 如果不是压缩文件，直接使用原文件
    UNZIPPED_FILE=&quot;$RESTORE_FILE&quot;
fi

# 检查目标数据库是否已存在
echo &quot;检查数据库 &apos;$DB_NAME&apos; 是否存在...&quot;
if psql -h &quot;$DB_HOST&quot; -p &quot;$DB_PORT&quot; -U &quot;$DB_USER&quot; -lqt | cut -d \| -f 1 | grep -qw &quot;$DB_NAME&quot;; then
    echo &quot;数据库 &apos;$DB_NAME&apos; 已存在，将清空数据库并继续恢复操作...&quot;

    # 清空数据库（危险操作！）
    PGPASSWORD=&quot;$DB_PASS&quot; psql -h &quot;$DB_HOST&quot; -p &quot;$DB_PORT&quot; -U &quot;$DB_USER&quot; -d &quot;$DB_NAME&quot; -c &quot;
    BEGIN;
    -- 关闭所有连接（避免锁表）
    SELECT pg_terminate_backend(pid)
    FROM pg_stat_activity
    WHERE datname = &apos;$DB_NAME&apos; AND pid &amp;#x3C;&gt; pg_backend_pid();

    -- 清空所有模式下的对象（注意：会删除所有数据！）
    DROP SCHEMA public CASCADE;
    CREATE SCHEMA public;
    GRANT ALL ON SCHEMA public TO public;
    GRANT ALL ON SCHEMA public TO $DB_USER;
    COMMIT;
    &quot;

    if [ $? -ne 0 ]; then
        echo &quot;错误：清空数据库失败！&quot;
        exit 1
    fi
else
    echo &quot;数据库 &apos;$DB_NAME&apos; 不存在，将创建新数据库...&quot;
    # 创建新数据库
    PGPASSWORD=&quot;$DB_PASS&quot; createdb -h &quot;$DB_HOST&quot; -p &quot;$DB_PORT&quot; -U &quot;$DB_USER&quot; &quot;$DB_NAME&quot;
    if [ $? -ne 0 ]; then
        echo &quot;错误：创建数据库失败！&quot;
        exit 1
    fi
fi

# 执行恢复
echo &quot;开始从 $RESTORE_FILE 恢复数据库 $DB_NAME...&quot;

# 使用环境变量设置密码，避免在命令行中明文传递
PGPASSWORD=&quot;$DB_PASS&quot; pg_restore -h &quot;$DB_HOST&quot; -p &quot;$DB_PORT&quot; -U &quot;$DB_USER&quot; -d &quot;$DB_NAME&quot; -v &quot;$UNZIPPED_FILE&quot;

# 检查恢复是否成功
if [ $? -eq 0 ]; then
    echo &quot;恢复完成！&quot;
else
    echo &quot;错误：恢复失败！&quot;
    exit 1
fi

# 清理临时文件
echo &quot;清理临时文件...&quot;
rm -rf &quot;$TEMP_DIR&quot;
echo &quot;恢复过程已完成！&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo bash restore_maxkb.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250606_431523.png&quot; alt=&quot;恢复完成&quot;&gt;{caption:恢复完成}&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.DDmf5UD8.jpg"/><enclosure url="/_astro/thumbnail.DDmf5UD8.jpg"/></item><item><title>WPF布局控件</title><link>https://saneko.me/blog/9f885850e83b</link><guid isPermaLink="true">https://saneko.me/blog/9f885850e83b</guid><description>简要介绍了 WPF 常用布局控件的功能及使用方法。</description><pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { ProsCons } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;前提&lt;/h3&gt;
&lt;p&gt;在 WPF 开发中，布局控件是构建灵活、自适应界面的核心。合理选择布局控件能显著提升界面的美观度和用户体验。本文将详细介绍 WPF 中常见的布局控件，分析它们的特性差异，并结合实际场景给出选型建议。&lt;/p&gt;
&lt;h3&gt;控件总览&lt;/h3&gt;
&lt;p&gt;| &lt;strong&gt;布局类型&lt;/strong&gt; | &lt;strong&gt;核心控件&lt;/strong&gt;  | &lt;strong&gt;布局特点&lt;/strong&gt;                            |
| ------------ | ------------- | --------------------------------------- |
|   栈式布局   | &lt;code&gt;StackPanel&lt;/code&gt;  | 子元素按水平或垂直方向线性排列          |
|   网格布局   | &lt;code&gt;Grid&lt;/code&gt;        | 基于行 / 列网格的二维布局，支持混合尺寸 |
|   停靠布局   | &lt;code&gt;DockPanel&lt;/code&gt;   | 子元素停靠在容器边缘或填充剩余空间      |
|   流式布局   | &lt;code&gt;WrapPanel&lt;/code&gt;   | 子元素自动换行排列，适应容器尺寸变化    |
|   均分布局   | &lt;code&gt;UniformGrid&lt;/code&gt; | 子元素等宽等高，自动分配空间            |
|   绝对定位   | &lt;code&gt;Canvas&lt;/code&gt;      | 基于坐标（X/Y）的绝对定位布局           |&lt;/p&gt;
&lt;h3&gt;控件详解&lt;/h3&gt;
&lt;h4&gt;StackPanel&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;排列方式&lt;/strong&gt;：通过&lt;code&gt;Orientation&lt;/code&gt;属性控制水平（Horizontal）或垂直（Vertical）排列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尺寸逻辑&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;主轴（排列方向）：子元素按MinWidth/MinHeight或内容大小占据空间。&lt;/li&gt;
&lt;li&gt;次轴（垂直方向）：默认拉伸填充（可通过HorizontalAlignment/VerticalAlignment调整）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;简单的单行 / 单列布局（如工具栏、表单垂直排列）。&lt;/li&gt;
&lt;li&gt;子元素数量较少且无需复杂排版的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;StackPanel Orientation=&quot;Vertical&quot; Margin=&quot;10&quot;&gt;
    &amp;#x3C;Button Content=&quot;按钮1&quot; Margin=&quot;5&quot; /&gt;
    &amp;#x3C;Button Content=&quot;按钮2&quot; Margin=&quot;5&quot; /&gt;
&amp;#x3C;/StackPanel&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Grid&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二维网格&lt;/strong&gt;：通过&lt;code&gt;RowDefinitions&lt;/code&gt;和&lt;code&gt;ColumnDefinitions&lt;/code&gt;定义行/列，支持星号（*）比例分配、固定尺寸（Auto）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨单元格布局&lt;/strong&gt;：通过&lt;code&gt;Grid.RowSpan&lt;/code&gt;和&lt;code&gt;Grid.ColumnSpan&lt;/code&gt;实现元素跨行列显示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;复杂界面布局（如数据表格、多区域窗口）。&lt;/li&gt;
&lt;li&gt;需要响应式设计（如不同分辨率下的自适应布局）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;Grid&gt;
    &amp;#x3C;Grid.RowDefinitions&gt;
        &amp;#x3C;RowDefinition Height=&quot;Auto&quot; /&gt; &amp;#x3C;!-- 标题行，自动适应内容 --&gt;
        &amp;#x3C;RowDefinition Height=&quot;*&quot; /&gt;    &amp;#x3C;!-- 内容行，填充剩余空间 --&gt;
    &amp;#x3C;/Grid.RowDefinitions&gt;
    &amp;#x3C;TextBlock Grid.Row=&quot;0&quot; Text=&quot;标题&quot; Margin=&quot;10&quot; /&gt;
    &amp;#x3C;TextBox Grid.Row=&quot;1&quot; Margin=&quot;10&quot; /&gt;
&amp;#x3C;/Grid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;DockPanel&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;停靠逻辑&lt;/strong&gt;：子元素通过Dock属性停靠在容器边缘，最后一个元素默认填充剩余空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;应用程序主窗口布局（如顶部菜单、左侧导航栏、中间内容区）。&lt;/li&gt;
&lt;li&gt;固定边缘元素与自适应内容结合的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;DockPanel&gt;
    &amp;#x3C;Menu DockPanel.Dock=&quot;Top&quot; /&gt;          &amp;#x3C;!-- 顶部菜单 --&gt;
    &amp;#x3C;TreeView DockPanel.Dock=&quot;Left&quot; /&gt;      &amp;#x3C;!-- 左侧导航 --&gt;
    &amp;#x3C;TextBox DockPanel.Dock=&quot;Fill&quot; /&gt;       &amp;#x3C;!-- 填充剩余空间 --&gt;
&amp;#x3C;/DockPanel&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;WrapPanel&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自动换行&lt;/strong&gt;：子元素按主轴方向排列，超出容器尺寸时自动换行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;动态数量的子元素（如标签云、图片画廊）。&lt;/li&gt;
&lt;li&gt;需要根据窗口大小自动调整排列方式的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;WrapPanel Orientation=&quot;Horizontal&quot;&gt;
    &amp;#x3C;Button Content=&quot;短文本&quot; /&gt;
    &amp;#x3C;Button Content=&quot;较长的文本需要换行&quot; /&gt;
    &amp;#x3C;Button Content=&quot;另一个按钮&quot; /&gt;
&amp;#x3C;/WrapPanel&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;UniformGrid&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;等宽等高&lt;/strong&gt;：所有子元素自动占据相同大小的单元格，行列数由Rows或Columns属性决定（或自动计算）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;宫格布局（如应用图标列表、参数配置面板）。&lt;/li&gt;
&lt;li&gt;子元素数量固定且需要均匀分布的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;UniformGrid Rows=&quot;3&quot; Columns=&quot;3&quot; Margin=&quot;10&quot;&gt;
    &amp;#x3C;Button Content=&quot;1&quot; /&gt;
    &amp;#x3C;Button Content=&quot;2&quot; /&gt;
    &amp;#x3C;!-- 省略其他按钮 --&gt;
&amp;#x3C;/UniformGrid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Canvas&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;坐标定位&lt;/strong&gt;：子元素通过&lt;code&gt;Canvas.Left&lt;/code&gt;和&lt;code&gt;Canvas.Top&lt;/code&gt;属性指定绝对位置，支持像素级精确控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;图形绘制（如流程图、自定义控件设计）。&lt;/li&gt;
&lt;li&gt;需要固定位置的元素（如浮层提示、动态定位图标）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;Canvas&gt;
    &amp;#x3C;Ellipse Canvas.Left=&quot;50&quot; Canvas.Top=&quot;50&quot; Width=&quot;30&quot; Height=&quot;30&quot; Fill=&quot;Red&quot; /&gt;
    &amp;#x3C;TextBlock Canvas.Left=&quot;100&quot; Canvas.Top=&quot;60&quot; Text=&quot;坐标点&quot; /&gt;
&amp;#x3C;/Canvas&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;建议总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;简单线性排列 → &lt;code&gt;StackPanel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;复杂二维布局 → &lt;code&gt;Grid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;边缘固定 + 中心自适应 → &lt;code&gt;DockPanel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;动态换行内容 → &lt;code&gt;WrapPanel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;均匀分布宫格 → &lt;code&gt;UniformGrid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;像素级精确控制 → &lt;code&gt;Canvas&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/thumbnail.5heLl8Oh.jpg"/><enclosure url="/_astro/thumbnail.5heLl8Oh.jpg"/></item><item><title>WPF创建简单用户控件</title><link>https://saneko.me/blog/b1662f148546</link><guid isPermaLink="true">https://saneko.me/blog/b1662f148546</guid><description>介绍如何在WPF中创建和使用简单用户控件，包含依赖属性和自定义事件的实现方法。</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;用户控件(User Control)&lt;/code&gt;​​是一种可重用的UI组件，通过组合现有控件来创建自定义功能模块。它本质上是一个容器，可以包含其他控件并封装其交互逻辑。&lt;/p&gt;
&lt;h2&gt;主要特点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;​继承自UserControl类​​.&lt;/li&gt;
&lt;li&gt;​XAML+代码后置.&lt;/li&gt;
&lt;li&gt;可重用​​的独立功能单元.&lt;/li&gt;
&lt;li&gt;可自定义属性、方法和事件​.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心优势&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;开发效率提升&lt;/li&gt;
&lt;li&gt;维护性增强&lt;/li&gt;
&lt;li&gt;功能解耦&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;p&gt;假设需要封装一个&lt;code&gt;FileSelectorBox&lt;/code&gt;控件，用来实现文件选择的功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控件组成：
由一个TextBlock和一个Button组成。&lt;/li&gt;
&lt;li&gt;依赖属性：
FilePath(当前选择的文件路径)&lt;/li&gt;
&lt;li&gt;自定义事件：
OnFileChanged(当文件路径发生改变)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;创建用户控件&lt;/h4&gt;
&lt;p&gt;右键项目，选择&lt;code&gt;添加&lt;/code&gt;，选择&lt;code&gt;用户控件&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250513_135731.png&quot; alt=&quot;创建控件&quot;&gt;&lt;/p&gt;
&lt;h4&gt;组合现有控件&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;Grid&gt;
    &amp;#x3C;Grid.ColumnDefinitions&gt;
        &amp;#x3C;ColumnDefinition Width=&quot;*&quot;/&gt;
        &amp;#x3C;ColumnDefinition Width=&quot;Auto&quot;/&gt;
    &amp;#x3C;/Grid.ColumnDefinitions&gt;
    &amp;#x3C;TextBlock Grid.Column=&quot;0&quot;/&gt;
    &amp;#x3C;Button Grid.Column=&quot;1&quot; Content=&quot;选择&quot; Width=&quot;71&quot;/&gt;
&amp;#x3C;/Grid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;添加依赖属性和自定义事件&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public event EventHandler OnFileChanged;

public static readonly DependencyProperty FilePathProperty =
DependencyProperty.Register(
    &quot;FilePath&quot;,     // 属性名称
    typeof(string),   // 属性类型
    typeof(FileSelectorBox), // 所属类型
    new PropertyMetadata(null, OnValuesChanged)); // 默认值，添加回调方法

public string FilePath
{
    get { return (string)GetValue(FilePathProperty); }
    set { SetValue(FilePathProperty, value); OnFileChanged?.Invoke(this, EventArgs.Empty); }
}

private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    // 回调方法
    // 外部修改依赖属性的值时，将在这里回调...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当FilePath被修改时，需要修改TextBlock的内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;TextBlock Grid.Column=&quot;0&quot; Text=&quot;{Binding FilePath,
          RelativeSource={RelativeSource AncestorType=UserControl}}&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;按钮功能&lt;/h4&gt;
&lt;p&gt;当按钮点击时，需要弹出一个文件选择框来选择文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;Button Grid.Column=&quot;1&quot; Content=&quot;选择&quot; Width=&quot;71&quot; Click=&quot;Button_Click&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;private void Button_Click(object sender, RoutedEventArgs e)
{
    Button button = (Button)sender;
    switch(button.Content)
    {
        case &quot;选择&quot;:
            OpenFileDialog openFileDialog = new()
            {
                Title = &quot;选择文件&quot;,
                Filter = &quot;All File|*.*&quot;
            };
            openFileDialog.ShowDialog();
            FilePath = openFileDialog.FileName;
            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;控件使用&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;Grid&gt;
    &amp;#x3C;local:FileSelectorBox  FilePath=&quot;{Binding ExternalFilePath, Mode=TwoWay}&quot; OnFileChanged=&quot;FileSelectorBox_OnFileChanged&quot;/&gt;
&amp;#x3C;/Grid&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public partial class ToolMainWindow : UserControl, INotifyPropertyChanged
{
    private string _externalFilePath = string.Empty;  // 数据源，也可以是自定义的数据模型

    public string ExternalFilePath
    {
        get { return _externalFilePath; }
        set { _externalFilePath = value; OnPropertyChanged(nameof(ExternalFilePath)); }  // 通知属性值已更改
    }

    public ToolMainWindow()
    {
        InitializeComponent();
        DataContext = this;  // 设置数据上下文
    }

    public event PropertyChangedEventHandler? PropertyChanged;  // 实现接口

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private void FileSelectorBox_OnFileChanged(object sender, EventArgs e)
    {
        Growl.Info($&quot;ExternalFilePath发生变化:{ExternalFilePath}&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250513_153823.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.B17pd6Tt.jpg"/><enclosure url="/_astro/thumbnail.B17pd6Tt.jpg"/></item><item><title>WPF错误的依赖属性仍然运行问题</title><link>https://saneko.me/blog/456cae0b31ef</link><guid isPermaLink="true">https://saneko.me/blog/456cae0b31ef</guid><description>本文简要说明了 WPF 中依赖属性声明错误的常见原因及解决方法。</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;前提&lt;/h3&gt;
&lt;p&gt;在 WPF 开发中，依赖属性（DependencyProperty）是一项核心技术，它让属性具有数据绑定、样式继承等强大功能。&lt;/p&gt;
&lt;p&gt;但在实际编码过程中，开发者可能会遇到一些 “奇怪” 的现象：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;即便代码写法不符合常规认知，程序依然能正常运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文将通过一个实际案例，深入剖析这种异常现象背后的原理，帮助大家更好地理解 WPF 依赖属性系统。&lt;/p&gt;
&lt;h3&gt;问题代码复现&lt;/h3&gt;
&lt;p&gt;以下是一段定义&lt;code&gt;FileSelectorBox&lt;/code&gt;用户控件依赖属性的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public partial class FileSelectorBox : UserControl
{

    public static readonly DependencyProperty PropertyAProperty =
    DependencyProperty.Register(
        &quot;PropertyA&quot;,     // 属性名称
        typeof(bool),   // 属性类型
        typeof(FileSelectorBox), // 所属类型
        new PropertyMetadata(false, OnValuesChanged)); // 默认值，添加回调方法

    public bool PropertyA
    {
        get { return (bool)GetValue(PropertyAProperty); }
        set { SetValue(PropertyAProperty, value);  }
    }

    public static readonly DependencyProperty PropertyBProperty =
    DependencyProperty.Register(
        &quot;PropertyB&quot;,     // 属性名称
        typeof(bool),   // 属性类型
        typeof(FileSelectorBox), // 所属类型
        new PropertyMetadata(false, OnValuesChanged)); // 默认值，添加回调方法

    public bool PropertyB
    {
        get { return (bool)GetValue(PropertyAProperty); }
        set { SetValue(PropertyAProperty, value); }
    }

    // ....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;乍一看，&lt;code&gt;PropertyB&lt;/code&gt;属性的get和set方法中，调用GetValue和SetValue时使用的是&lt;code&gt;PropertyAProperty&lt;/code&gt;，而不是正确的&lt;code&gt;PropertyBProperty&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;按照常理，这样的代码应该会导致属性值错乱。但实际运行时却没有立即暴露出问题，这是为什么呢？&lt;/p&gt;
&lt;h3&gt;现象背后的原理&lt;/h3&gt;
&lt;h4&gt;依赖属性系统的底层机制&lt;/h4&gt;
&lt;p&gt;WPF 依赖属性系统的底层存储由&lt;code&gt;DependencyObject&lt;/code&gt;类维护，通过&lt;code&gt;DependencyProperty&lt;/code&gt;对象作为唯一标识（如PropertyAProperty和PropertyBProperty）来访问和存储属性值。系统在操作属性时，直接通过这些&lt;code&gt;DependencyProperty&lt;/code&gt;键进行查找和修改，与 CLR 属性包装器的名称和实现细节并无直接关联。&lt;/p&gt;
&lt;h4&gt;XAML 解析的特性&lt;/h4&gt;
&lt;p&gt;当我们在 XAML 中设置属性，例如，XAML 解析器会直接调用SetValue(PropertyBProperty, true)，它绕过了 CLR 属性包装器，直接操作底层的依赖属性存储。因此，即使 CLR 属性包装器中的get和set方法存在错误引用，XAML 解析器依然能正确设置PropertyBProperty的值。&lt;/p&gt;
&lt;h4&gt;代码访问的矛盾&lt;/h4&gt;
&lt;p&gt;虽然 XAML 解析能正确设置属性值，但当在 C# 代码中直接访问PropertyB属性时，问题就会暴露出来。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;FileSelectorBox fileSelectorBox = new();
fileSelectorBox.PropertyB = true; // 实际修改的是PropertyAProperty的值
bool value = fileSelectorBox.PropertyB; // 实际获取的是PropertyAProperty的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种不一致的行为，会导致属性值的读写混乱，尤其在复杂的业务逻辑和数据绑定场景中，可能引发难以排查的 Bug。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;WPF 的依赖属性系统实际上有两层结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;底层存储：由DependencyObject类内部维护的哈希表，通过DependencyProperty键访问（例如PropertyBProperty）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上层接口：为了让依赖属性能够像普通属性一样被 C# 代码使用，需要提供一个 CLR 属性包装器&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;xaml直接访问的依赖属性的底层存储，因此绕过了上层的CLR包装器。在开发过程中，我们必须确保 CLR 属性包装器正确地映射到对应的 DependencyProperty，以避免潜在的错误。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.IBwl5kB3.jpg"/><enclosure url="/_astro/thumbnail.IBwl5kB3.jpg"/></item><item><title>Maxkb加载gpt2失败</title><link>https://saneko.me/blog/6bd412ba6288</link><guid isPermaLink="true">https://saneko.me/blog/6bd412ba6288</guid><description>本文介绍Maxkb加载gpt2失败的原因及解决方法。</description><pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;问题描述&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Maxkb&lt;/code&gt;使用AI高级编排时，使用AI对话会抛出&lt;code&gt;Can’t load tokenizer for ‘gpt2’&lt;/code&gt;的异常&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Can&apos;t load tokenizer for &apos;gpt2. if you were trying to load it from &quot;htps://huggingface.co/models, make sure you don&apos;t have a
local directory with the same name. 0therwise, make sure &apos;gpt2 is the correct path to a directory containing all relevant files for
a GPT2TokenizerFast tokenizer.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250425_090427.png&quot; alt=&quot;错误日志&quot;&gt;{caption:错误日志}&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;p&gt;这是由于用于计算&lt;code&gt;token&lt;/code&gt;的gpt2没有下载到本地导致的&lt;/p&gt;
&lt;p&gt;可以通过&lt;code&gt;huggingface&lt;/code&gt;进行下载&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install -U huggingface_hub   # 安装huggingface库
huggingface-cli download --resume-download gpt2 --local-dir gpt2  # 下载gpt2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将下载完成后的模型放在&lt;code&gt;/opt/maxkb/app/models/tokenizer/gpt2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;修改Maxkb中查找gpt2的路径&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# apps\common\config\tokenizer_manage_config.py
class TokenizerManage:
    tokenizer = None

    @staticmethod
    def get_tokenizer():
        from transformers import GPT2TokenizerFast
        if TokenizerManage.tokenizer is None:
            TokenizerManage.tokenizer = GPT2TokenizerFast.from_pretrained(
                &apos;/opt/maxkb/app/models/tokenizer/gpt2&apos;,
                cache_dir=&quot;/opt/maxkb/model/tokenizer&quot;,
                local_files_only=True,
                resume_download=False,
                force_download=False)
        return TokenizerManage.tokenizer
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.ByKrVPYB.jpg"/><enclosure url="/_astro/thumbnail.ByKrVPYB.jpg"/></item><item><title>ubuntu22.04源码部署maxkb</title><link>https://saneko.me/blog/a77426832765</link><guid isPermaLink="true">https://saneko.me/blog/a77426832765</guid><description>本文介绍在Ubuntu 22.04环境下源码部署Maxkb的完整流程，适用于AI应用部署场景。</description><pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;源码下载&lt;/h2&gt;
&lt;p&gt;从&lt;a href=&quot;https://github.com/1Panel-dev/MaxKB&quot;&gt;maxkb项目&lt;/a&gt;中拉取最新源码.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_091220.png&quot; alt=&quot;拉取源码&quot;&gt;&lt;/p&gt;
&lt;h2&gt;前端依赖&lt;/h2&gt;
&lt;h3&gt;Nodejs&lt;/h3&gt;
&lt;p&gt;前端依赖下载需要使用较高版本的Nodejs，使用apt查看可下载的Node的版本信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;apt list -a nodejs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_092014.png&quot; alt=&quot;nodejs版本&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现没有较新的22版本，运行下面的命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# 安装完成后，检查 Node.js 和 npm 的版本
node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_092634.png&quot; alt=&quot;更新nodejs版本&quot;&gt;&lt;/p&gt;
&lt;h3&gt;下载源&lt;/h3&gt;
&lt;p&gt;安装好npm后，需要配置npm源，建议使用以下命令添加国内源&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm config set registry https://registry.npmmirror.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;p&gt;使用以下命令安装前端所需依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ui   # 需要先切换至ui目录
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_093133.png&quot; alt=&quot;下载依赖项&quot;&gt;&lt;/p&gt;
&lt;h2&gt;后端依赖&lt;/h2&gt;
&lt;h3&gt;python&lt;/h3&gt;
&lt;p&gt;python版本不能低于3.11,使用以下命令进行安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update  # 更新系统包列表
sudo apt install software-properties-common -y # ​安装依赖项
sudo add-apt-repository ppa:deadsnakes/ppa # 添加 Deadsnakes PPA
sudo apt install python3.11  # ​安装 Python 3.11
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;poetry&lt;/h3&gt;
&lt;p&gt;通过以下命令进行安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install poetry==1.8.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;p&gt;使用以下命令安装后端所需依赖(需要较长时间)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ..   # 需要先切换至项目根目录
poetry install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_095147.png&quot; alt=&quot;安装依赖&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Postgresql(数据库)&lt;/h2&gt;
&lt;h3&gt;安装Postgresql&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo sh -c &apos;echo &quot;deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main&quot; &gt; /etc/apt/sources.list.d/pgdg.list&apos;
wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &amp;#x26;&gt;/dev/null  # 启用它的官方包存储库
sudo apt update  # 获取包的最新版本
sudo apt install postgresql postgresql-client postgresql-server-dev-17 -y   # 安装服务端，客户端，开发套件
sudo systemctl status postgresql  # 验证 PostgreSQL 服务是否启动并运行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_111914.png&quot; alt=&quot;安装Postgresql&quot;&gt;&lt;/p&gt;
&lt;h3&gt;更新密码&lt;/h3&gt;
&lt;p&gt;使用下面命令进入psql, 并更新管理员用户密码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo -u postgres psql
ALTER USER postgres PASSWORD &apos;123123&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入&lt;code&gt;\q&lt;/code&gt;退出&lt;/p&gt;
&lt;h3&gt;安装插件&lt;/h3&gt;
&lt;p&gt;执行以下指令，安装vector插件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /tmp
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果make命令报错，可能需要额外安装gcc和make工具:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;apt install make gcc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建数据库&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;psql -h localhost -U postgres&lt;/code&gt;输入密码并登录数据库，按行执行下面的sql&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE DATABASE &quot;maxkb&quot;;
\c &quot;maxkb&quot;;
CREATE EXTENSION &quot;vector&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;将项目根目录下的&lt;code&gt;config_example.yml&lt;/code&gt;配置文件拷贝至/opt/maxkb/conf目录下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mkdir -p /opt/maxkb/conf
sudo cp config_example.yml /opt/maxkb/conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修在&lt;code&gt;/opt/maxkb/conf/config_example.yml&lt;/code&gt;内容如下（如果按照的是上面的步骤，则只需要修改 DB_USER: postgres和 DB_PASSWORD: 123123）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# 数据库链接信息
DB_NAME: maxkb
DB_HOST: localhost
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: 123123
DB_ENGINE: django.db.backends.postgresql_psycopg2

DEBUG: false

TIME_ZONE: Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动&lt;/h2&gt;
&lt;h3&gt;启动后端&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 main.py start
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果抛出异常，可能是有些库没有安装。执行以下指令，安装缺少的库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动前端&lt;/h3&gt;
&lt;p&gt;需要在&lt;code&gt;ui&lt;/code&gt;目录下启动&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ui
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过浏览器打开&lt;code&gt;http://localhost:3000/ui/&lt;/code&gt;, 可以看到Maxkb的登录页面(第一次启动可能需要较长时间)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用户名：admin&lt;br&gt;
密码：MaxKB@123..&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_173630.png&quot; alt=&quot;前端页面&quot;&gt;&lt;/p&gt;
&lt;h2&gt;解除限制&lt;/h2&gt;
&lt;p&gt;需要修改以下文件&lt;/p&gt;
&lt;h3&gt;user_serializers.py&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改文件: apps/users/serializers/user_serializers.py&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改位置: 注释掉&lt;code&gt;valid_license&lt;/code&gt;修饰器。分别大概在188、775行左右&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改后:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_174828.png&quot; alt=&quot;user_serializers.py修改处1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_174837.png&quot; alt=&quot;user_serializers.py修改处2&quot;&gt;&lt;/p&gt;
&lt;h3&gt;application_serializers.py&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改文件: apps/application/serializers/application_serializers.py&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改位置: 注释掉&lt;code&gt;valid_license&lt;/code&gt;修饰器。分别大概在513、720行左右&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改后:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_180914.png&quot; alt=&quot;application_serializers.py修改处1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_180919.png&quot; alt=&quot;application_serializers.py修改处2&quot;&gt;&lt;/p&gt;
&lt;h3&gt;dataset_serializers.py&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改文件: apps/dataset/serializers/dataset_serializers.py&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改位置: 注释掉&lt;code&gt;valid_license&lt;/code&gt;修饰器。大概在412行左右&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改后:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_181132.png&quot; alt=&quot;dataset_serializers.py修改处1&quot;&gt;&lt;/p&gt;
&lt;h3&gt;valid_serializers.py&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;修改文件: apps/setting/serializers/valid_serializers.py&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改位置: 将count数量调整为&lt;code&gt;999999&lt;/code&gt;, 注释掉valid的判断过程，直接返回True&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改后:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_180445.png&quot; alt=&quot;valid_serializers.py修改处1&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Ollama&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;执行以下命令，安装ollama&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://ollama.com/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250318_085030.png&quot; alt=&quot;安装ollama&quot;&gt;&lt;/p&gt;
&lt;h3&gt;添加模型&lt;/h3&gt;
&lt;p&gt;浏览器打开MaxKB主界面:&lt;code&gt;http://localhost:3000/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;依次点击系统设置,模型设置，添加模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_183035.png&quot; alt=&quot;添加模型&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250317_183045.png&quot; alt=&quot;填写API&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.CxsoRjsl.jpg"/><enclosure url="/_astro/thumbnail.CxsoRjsl.jpg"/></item><item><title>MCDR安装与使用</title><link>https://saneko.me/blog/c1fc00e76fbf</link><guid isPermaLink="true">https://saneko.me/blog/c1fc00e76fbf</guid><description>本文简要介绍了MCDR的安装与使用方法。</description><pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Video } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://mcdreforged.com&quot;&gt;MCDR（MCDaemon Reforged）&lt;/a&gt;是由&lt;strong&gt;Fallen_Breath&lt;/strong&gt;主导维护的一个基于Python的工具，用于管理Minecraft服务器。&lt;/p&gt;
&lt;p&gt;它通过插件系统提供丰富的功能扩展，如自动备份、玩家高亮等。&lt;/p&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MCDR通过&lt;code&gt;Popen&lt;/code&gt;来启动Minecraft服务端作为子进程，进而控制服务端的输入与输出流。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MCDR通过解析服务端的输出，来抽象为不同的事件，并将它们分派给插件以进行响应。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;借助Minecraft的命令系统，MCDR也可以通过标准输入流发送Minecraft命令以操作Minecraft服务端。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250312_210827.png&quot; alt=&quot;原理图&quot;&gt;&lt;/p&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开服务器后端，直接通过pip进行安装&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip3 install mcdreforged
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;在服务端中，新建一个文件夹(例如mcdr_server)用来存放脚本，再通过init方法进行初始化&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd mcdr_server
mcdreforged init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时会生成如下所示的文件夹结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;mcdr_server/
    ├─ config/
    ├─ logs/
    │   └─ MCDR.log
    ├─ plugins/
    ├─ server/
    ├─ config.yml
    └─ permission.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;将我们整个服务端，放到server文件夹下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;mcdr_server/
    ├─ config/
    ├─ logs/
    │   └─ MCDR.log
    ├─ plugins/
    ├─ server/
++  │   ├─ ...
++  │   ├─ minecraft_server.jar
++  │   └─ server.properties
    ├─ config.yml
    └─ permission.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;p&gt;通过编辑&lt;code&gt;config.yml&lt;/code&gt;文件来配置 MCDR&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;language: zh_cn # 输出信息的语言
working_directory: server # 服务端的工作目录
start_command: java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -Xms3G -Xmx7G -jar fabric-server.jar nogui
handler: vanilla_handler # 用于 原版/Carpet/Fabric服务端 无需修改
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动&lt;/h3&gt;
&lt;p&gt;输入以下命令进行启动&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mcdreforged
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;服务端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250312_214316.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250312_220516.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;插件&lt;/h3&gt;
&lt;h4&gt;Here&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;安装指令&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;!!MCDR plugin install here
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;!!here&lt;/code&gt;：显示玩家坐标并使其发光&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;效果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250312_220846.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Prime Backup&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;安装指令&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;!!MCDR plugin install prime_backup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置config/prime_backup文件夹下的&lt;code&gt;config.json&lt;/code&gt;, 将enabled设置为true.&lt;/p&gt;
&lt;p&gt;其他配置参考&lt;a href=&quot;https://tisunion.github.io/PrimeBackup/zh/&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;!!pb help [指令]: 展示全部指令/给定指令的详细帮助

!!pb make [注释]: 创建一个备份。注释为可选注释

!!pb back [备份ID]: 回档至给定备份

!!pb list [...]: 列出备份，展示备份列表

!!pb show [备份ID]: 展示给定备份的详细信息

!!pb rename 备份ID 新注释: 修改给定备份的注释

!!pb delete 备份ID [备份ID]: 删除给定备份。可输入多个备份ID

!!pb confirm: 确认当前的任务操作

!!pb abort: 终止当前的任务操作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更多命令可通过&lt;code&gt;!!pb help&lt;/code&gt;查看&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;效果&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/thumbnail.Dbiqce8_.jpg"/><enclosure url="/_astro/thumbnail.Dbiqce8_.jpg"/></item><item><title>华为云部署minecraft服务端</title><link>https://saneko.me/blog/d2ae16a72013</link><guid isPermaLink="true">https://saneko.me/blog/d2ae16a72013</guid><description>利用Fabric工具链生成Minecraft可读化源码的原理与流程。</description><pubDate>Sun, 09 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;前期准备&lt;/h2&gt;
&lt;h3&gt;1. 服务器&lt;/h3&gt;
&lt;p&gt;选用的是华为云的&lt;code&gt;Flexus云服务器X实例&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4核8G&lt;/li&gt;
&lt;li&gt;5M带宽&lt;/li&gt;
&lt;li&gt;SSD存储&lt;/li&gt;
&lt;li&gt;操作系统Huawei Cloud EulerOS 2.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_225056.png&quot; alt=&quot;服务器实例&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2. 登录&lt;/h3&gt;
&lt;p&gt;通过&lt;a href=&quot;https://www.hostbuf.com/&quot;&gt;FinalShell&lt;/a&gt;，登录服务器后台。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_230254.png&quot; alt=&quot;登录后台&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3. JDK&lt;/h3&gt;
&lt;p&gt;从&lt;a href=&quot;https://www.oracle.com/cn/java/technologies/downloads/#java21&quot;&gt;oracle官网&lt;/a&gt;下载jdk21&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250310_211000.png&quot; alt=&quot;下载jdk&quot;&gt;&lt;/p&gt;
&lt;p&gt;将下载好后的压缩包，通过FinalShell上传到服务器并解压&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tar -zxvf jdk-21_linux-x64_bin.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面开始配置环境变量&lt;/p&gt;
&lt;p&gt;编辑&lt;code&gt;/etc/profile&lt;/code&gt;文件以设置Java环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo vim /etc/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件末尾添加以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export JAVA_HOME=/root/jdk/jdk-21.0.6
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;激活配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /etc/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -version
# 输出一下内容，则表示安装成功
# java version &quot;21.0.6&quot; 2025-01-21 LTS
# Java(TM) SE Runtime Environment (build 21.0.6+8-LTS-188)
# Java HotSpot(TM) 64-Bit Server VM (build 21.0.6+8-LTS-188, mixed mode, sharing)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Fabric&lt;/h3&gt;
&lt;p&gt;访问&lt;a href=&quot;https://fabricmc.net/use/server/&quot;&gt;fabric官网&lt;/a&gt;，下载服务器核心，并上传到服务器上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250310_205749.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;启动&lt;/h2&gt;
&lt;h3&gt;1. 首次启动&lt;/h3&gt;
&lt;p&gt;执行以下指令，启动服务端&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 将fabric下载下来的文件重命名为fabric-server.jar，方便执行
# （-Xmx7G表示最大7GB内存，根据服务器配置调整）
java -Xmx7G -jar fabric-server.jar nogui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首次启动会失败，并会生成&lt;code&gt;eula.txt&lt;/code&gt;文件，需要我们编辑同意协议。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo vim eula.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;# 将eula=false修改为true
eula=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 配置规则&lt;/h3&gt;
&lt;p&gt;通过修改&lt;code&gt;server.properties&lt;/code&gt;来配置服务器规则&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;#Minecraft server properties
#Mon Mar 10 21:18:45 CST 2025

accepts-transfers=false
# 是否接受玩家转移（例如从一个服务器到另一个服务器）

allow-flight=false
# 是否允许玩家在生存模式下飞行。

allow-nether=true
# 是否启用下界维度

broadcast-console-to-ops=true
# 是否将控制台消息广播给操作员

broadcast-rcon-to-ops=true
# 是否将RCON命令输出广播给操作员

bug-report-link=
# 错误报告链接地址

difficulty=hard
# 游戏难度级别

enable-command-block=false
# 是否启用命令方块

enable-jmx-monitoring=false
# 是否启用JMX监控

enable-query=false
# 是否启用查询协议以获取服务器信息

enable-rcon=false
# 是否启用远程控制台功能

enable-status=true
# 是否启用状态请求响应

enforce-secure-profile=true
# 是否强制执行安全配置文件

enforce-whitelist=false
# 是否强制使用白名单

entity-broadcast-range-percentage=100
# 实体广播范围百分比

force-gamemode=false
# 是否强制所有玩家进入指定的游戏模式

function-permission-level=2
# 函数权限等级

gamemode=survival
# 默认游戏模式

generate-structures=true
# 是否生成结构物如村庄、要塞等

generator-settings={}
# 世界生成器设置

hardcore=false
# 是否启用硬核模式

hide-online-players=false
# 是否隐藏在线玩家列表

initial-disabled-packs=
# 初始禁用的功能包

initial-enabled-packs=vanilla
# 初始启用的功能包，默认是原版内容

level-name=world
# 保存的世界名称

level-seed=11311638121115121
# 世界的种子值

level-type=minecraft\:normal
# 世界的类型或地形风格

log-ips=true
# 是否记录IP地址
max-chained-neighbor-updates=1000000
# 最大连锁更新次数

max-players=20
# 服务器的最大玩家数量

max-tick-time=60000
# 每个tick的最大时间限制

max-world-size=29999984
# 世界大小上限

motd=A Minecraft Server
# 服务器的欢迎信息或描述

network-compression-threshold=256
# 网络压缩阈值

online-mode=true
# 是否启用在线模式验证

op-permission-level=4
# 管理员的操作权限等级

pause-when-empty-seconds=60
# 当没有玩家时暂停的时间

player-idle-timeout=0
# 玩家空闲超时时间（分钟），0表示不超时

prevent-proxy-connections=false
# 是否阻止代理连接

pvp=true
# 是否开启玩家之间的战斗

query.port=25565
# 查询端口

rate-limit=0
# 速率限制，0表示无限制

rcon.password=
# RCON密码

rcon.port=25575
# RCON端口

region-file-compression=deflate
# 区域文件压缩方式

require-resource-pack=false
# 是否要求资源包

resource-pack=
# 资源包URL

resource-pack-id=
# 资源包ID

resource-pack-prompt=
# 资源包提示信息

resource-pack-sha1=
# 资源包SHA-1哈希值

server-ip=
# 服务器绑定的IP地址

server-port=25565
# 服务器监听的端口号

simulation-distance=10
# 模拟距离

spawn-monsters=true
# 是否生成怪物

spawn-protection=16
# 出生点保护半径

sync-chunk-writes=true
# 是否同步写入区块数据

text-filtering-config=
# 是否同步写入区块数据

text-filtering-version=0
# 文本过滤版本

use-native-transport=true
# 是否使用本地传输优化

view-distance=10
# 视距范围

white-list=false
# 是否启用白名单
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 一键运行&lt;/h3&gt;
&lt;p&gt;新建一个&lt;code&gt;start.sh&lt;/code&gt;，写入启动指令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Xmx7G -jar fabric-server.jar nogui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续只要执行&lt;code&gt;sh start.sh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是此时如果关掉后台，服务器就停掉了。只需通过&lt;code&gt;nohup&lt;/code&gt;挂在后台即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nohup sh start.sh &amp;#x26;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 安全组&lt;/h3&gt;</content:encoded><h:img src="/_astro/thumbnail.CBbSG6Dg.jpg"/><enclosure url="/_astro/thumbnail.CBbSG6Dg.jpg"/></item><item><title>Activate Typora</title><link>https://saneko.me/blog/792bf387315d</link><guid isPermaLink="true">https://saneko.me/blog/792bf387315d</guid><description>This article introduces the steps and precautions for activating the Typora editor on the Windows system.</description><pubDate>Sun, 09 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Activation steps&lt;/h3&gt;
&lt;p&gt;Under Typora’s installation directory, open the folder &lt;code&gt;Typora\resources\page-dist\static\js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Open the JS file whose name starts with &lt;strong&gt;LicenseIndex&lt;/strong&gt;, e.g. &lt;code&gt;LicenseIndex.180dd4c7.bffb5802.chunk.js&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// # Change e.hasActivated=&quot;true&quot;==e.hasActivated to
e.hasActivated = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the file as administrator, then restart Typora.&lt;/p&gt;
&lt;h3&gt;Activated&lt;/h3&gt;
&lt;p&gt;The app will show as activated.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_232354.png&quot; alt=&quot;Activated&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.H4_3-tlN.jpg"/><enclosure url="/_astro/thumbnail.H4_3-tlN.jpg"/></item><item><title>minecraft模组：02创建工具</title><link>https://saneko.me/blog/cccf44be6a4a</link><guid isPermaLink="true">https://saneko.me/blog/cccf44be6a4a</guid><description>本文简要介绍了如何使用Fabric模组在Minecraft中创建一套铜质工具。</description><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;通过fabric编写模组，创建一套自定义工具&lt;code&gt;铜镐&lt;/code&gt;、 &lt;code&gt;铜锄&lt;/code&gt;、 &lt;code&gt;铜锹&lt;/code&gt;、 &lt;code&gt;铜斧&lt;/code&gt;、 &lt;code&gt;铜剑&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;工具材质&lt;/h3&gt;
&lt;p&gt;在 Minecraft 开发中，&lt;code&gt;ToolMaterials&lt;/code&gt; 类是一个非常重要的类，它主要用于定义工具的材质属性。&lt;/p&gt;
&lt;p&gt;首先，创建一个铜质的工具材料，统一管理&lt;code&gt;耐久度&lt;/code&gt;,&lt;code&gt;挖掘速度&lt;/code&gt;,&lt;code&gt;攻击力&lt;/code&gt;,&lt;code&gt;附魔能力&lt;/code&gt;等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 自定义的铜质工具材料类，实现了 ToolMaterial 接口，用于定义铜质工具的各项属性。
 */
public class CopperToolMaterial implements ToolMaterial {

    public static final CopperToolMaterial INSTANCE = new CopperToolMaterial();

    private final Supplier&amp;#x3C;Ingredient&gt; repairIngredient = () -&gt;
            Ingredient.ofItems(Items.COPPER_INGOT);

    // 耐久度  木制59/石制131/铁制250/金制32/钻石1561/下界合金2031
    @Override
    public int getDurability() { return 200; }

    // 挖掘速度乘数 木制2.0/石制4.0/铁制6.0/金制12.0/钻石8.0/下界合金9.0
    @Override
    public float getMiningSpeedMultiplier() {
        return 5.0f;
    }

    // 攻击伤害 木制0.0/石制1.0/铁制2.0/金制0.0/钻石4.0/下界合金4.0
    @Override
    public float getAttackDamage() {
        return 1.5f;
    }

    @Override
    public TagKey&amp;#x3C;Block&gt; getInverseTag() {
        return BlockTags.INCORRECT_FOR_STONE_TOOL;
    }

    // 附魔能力 木制15/石制5/铁制14/金制22/钻石10/下界合金15
    @Override
    public int getEnchantability() {
        return 10;
    }

    @Override
    public Ingredient getRepairIngredient() {
        return repairIngredient.get();
    }

    // 静态方法，用于注册物品到游戏的物品注册表中
    public static Item register(Item item, String id) {
        // 创建物品的标识符
        Identifier itemID = Identifier.of(&quot;mymod&quot;, id);
        // 将物品注册到游戏的物品注册表中，并返回注册后的物品实例
        return Registry.register(Registries.ITEM, itemID, item);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注册物品&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;register&lt;/code&gt;方法进行注册。分别将铜镐、铜锄、铜锹、铜斧和铜剑的&lt;code&gt;攻击伤害&lt;/code&gt;和&lt;code&gt;攻击速度&lt;/code&gt;修改为&lt;code&gt;[3.5, 1, 4, 9, 5.5]&lt;/code&gt;与&lt;code&gt;[1.2, 2, 1, 0.8, 1.6]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class CopperPickaxe {
    // 创建一个日志记录器
    public static final Logger LOGGER = LoggerFactory.getLogger(&quot;MyMod&quot;);

    // 定义一个静态的铜镐物品实例, 并通过 register 方法将该物品注册到游戏中
    public static final Item copperPickaxe = CopperToolMaterial.register(
            new PickaxeItem(CopperToolMaterial.INSTANCE,
                    new Item.Settings().attributeModifiers(PickaxeItem.createAttributeModifiers(CopperToolMaterial.INSTANCE, 1, -2.8f))),
            &quot;copper_pickaxe&quot;
    );

    // 初始化
    public static void initialize() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS)
                .register((itemGroup) -&gt; itemGroup.add(copperPickaxe));
        LOGGER.info(&quot;[DEBUG] 铜镐注册完成&quot;);
    }
}

// 铜锄
public class CopperHoe {
    // 创建一个日志记录器
    public static final Logger LOGGER = LoggerFactory.getLogger(&quot;MyMod&quot;);

    // 定义一个静态的铜锄物品实例, 并通过 register 方法将该物品注册到游戏中
    public static final Item CopperHoe = CopperToolMaterial.register(
            new HoeItem(CopperToolMaterial.INSTANCE,
                    new Item.Settings().attributeModifiers(HoeItem.createAttributeModifiers(CopperToolMaterial.INSTANCE, -1.5f, -2))),
            &quot;copper_hoe&quot;
    );

    // 初始化
    public static void initialize() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS)
                .register((itemGroup) -&gt; itemGroup.add(CopperHoe));

        LOGGER.info(&quot;[DEBUG] 铜锄注册完成&quot;);
    }
}

// 铜锹
public class CopperShovel {
    // 创建一个日志记录器
    public static final Logger LOGGER = LoggerFactory.getLogger(&quot;MyMod&quot;);

    // 定义一个静态的铜锹物品实例, 并通过 register 方法将该物品注册到游戏中
    public static final Item CopperShovel = CopperToolMaterial.register(
            new ShovelItem(CopperToolMaterial.INSTANCE,
                    new Item.Settings().attributeModifiers(ShovelItem.createAttributeModifiers(CopperToolMaterial.INSTANCE, 1.5f, -3))),
            &quot;copper_shovel&quot;
    );

    // 初始化
    public static void initialize() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS)
                .register((itemGroup) -&gt; itemGroup.add(CopperShovel));

        LOGGER.info(&quot;[DEBUG] 铜锹注册完成&quot;);
    }
}

// 铜斧
public class CopperAxe {
    // 创建一个日志记录器
    public static final Logger LOGGER = LoggerFactory.getLogger(&quot;MyMod&quot;);

    // 定义一个静态的铜斧物品实例, 并通过 register 方法将该物品注册到游戏中
    public static final Item CopperAxe = CopperToolMaterial.register(
            new AxeItem(CopperToolMaterial.INSTANCE,
                    new Item.Settings().attributeModifiers(AxeItem.createAttributeModifiers(CopperToolMaterial.INSTANCE, 6.5f, -3.2f))),
            &quot;copper_axe&quot;
    );

    // 初始化
    public static void initialize() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS)
                .register((itemGroup) -&gt; itemGroup.add(CopperAxe));

        LOGGER.info(&quot;[DEBUG] 铜斧注册完成&quot;);
    }
}

// 铜剑
public class CopperSword {
    // 创建一个日志记录器
    public static final Logger LOGGER = LoggerFactory.getLogger(&quot;MyMod&quot;);

    // 定义一个静态的铜剑物品实例, 并通过 register 方法将该物品注册到游戏中
    public static final Item copperSword = CopperToolMaterial.register(
            new SwordItem(CopperToolMaterial.INSTANCE,
                    new Item.Settings().attributeModifiers(SwordItem.createAttributeModifiers(CopperToolMaterial.INSTANCE, 3, -2.4f))),
            &quot;copper_sword&quot;
    );

    // 初始化
    public static void initialize() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.COMBAT)
                .register((itemGroup) -&gt; itemGroup.add(copperSword));
        LOGGER.info(&quot;[DEBUG] 铜剑注册完成&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加翻译&lt;/h3&gt;
&lt;h4&gt;en_us&lt;/h4&gt;
&lt;p&gt;打开&lt;code&gt;main\resources\assets\mymod\lang\en_us.json&lt;/code&gt;文件，不存在则新建，添加英语翻译&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;item.mymod.copper_pickaxe&quot;: &quot;Copper Pickaxe&quot;,
  &quot;item.mymod.copper_axe&quot;: &quot;Copper Axe&quot;,
  &quot;item.mymod.copper_hoe&quot;: &quot;Copper Hoe&quot;,
  &quot;item.mymod.copper_shovel&quot;: &quot;Copper Shovel&quot;,
  &quot;item.mymod.copper_sword&quot;: &quot;Copper Sword&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;zh_ch&lt;/h4&gt;
&lt;p&gt;打开&lt;code&gt;main\resources\assets\mymod\lang\zh_ch.json&lt;/code&gt;文件，不存在则新建，添加中文翻译&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;item.mymod.copper_pickaxe&quot;: &quot;铜镐&quot;,
  &quot;item.mymod.copper_axe&quot;: &quot;铜斧&quot;,
  &quot;item.mymod.copper_hoe&quot;: &quot;铜锄&quot;,
  &quot;item.mymod.copper_shovel&quot;: &quot;铜锹&quot;,
  &quot;item.mymod.copper_sword&quot;: &quot;铜剑&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加纹理模型&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;main\resources\assets\mymod\models\item&lt;/code&gt;文件夹下，新建&lt;code&gt;copper_axe.json&lt;/code&gt;,&lt;code&gt;copper_hoe.json&lt;/code&gt;,&lt;code&gt;copper_pickaxe.json&lt;/code&gt;, &lt;code&gt;copper_shovel.json&lt;/code&gt;,&lt;code&gt;copper_sword.json&lt;/code&gt;等文件，写入对应模型文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_axe.json
{
  &quot;parent&quot;: &quot;item/handheld&quot;,
  &quot;textures&quot;: {
    &quot;layer0&quot;: &quot;mymod:item/copper_axe&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_hoe.json
{
  &quot;parent&quot;: &quot;item/handheld&quot;,
  &quot;textures&quot;: {
    &quot;layer0&quot;: &quot;mymod:item/copper_hoe&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_pickaxe.json
{
  &quot;parent&quot;: &quot;item/handheld&quot;,
  &quot;textures&quot;: {
    &quot;layer0&quot;: &quot;mymod:item/copper_pickaxe&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_shovel.json
{
  &quot;parent&quot;: &quot;item/handheld&quot;,
  &quot;textures&quot;: {
    &quot;layer0&quot;: &quot;mymod:item/copper_shovel&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_sword.json
{
  &quot;parent&quot;: &quot;item/handheld&quot;,
  &quot;textures&quot;: {
    &quot;layer0&quot;: &quot;mymod:item/copper_sword&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，此时&lt;strong&gt;parent&lt;/strong&gt;填入的是&lt;code&gt;item/handheld&lt;/code&gt;模型，表示物品将使用默认的手持物品显示方式。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;main\resources\assets\mymod\textures\item&lt;/code&gt;文件夹下,放入对应纹理文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_190327.png&quot; alt=&quot;工具纹理&quot;&gt;&lt;/p&gt;
&lt;h3&gt;合成&lt;/h3&gt;
&lt;p&gt;以上的代码已经可以在游戏里正常工作了，但是想要在生存模式中使用，就需要设计对应的合成方式。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;main\resources\data\mymod\recipe&lt;/code&gt;文件夹下，分别新建对应工具的json文件，写入合成方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;铜镐&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_pickaxe.json
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot;ccc&quot;,
    &quot; / &quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_pickaxe&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_191955.png&quot; alt=&quot;铜镐&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;铜铲&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_shovel.json
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot; c &quot;,
    &quot; / &quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_shovel&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_192619.png&quot; alt=&quot;铜铲&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;铜锄&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_hoe.json
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot;cc &quot;,
    &quot; / &quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_hoe&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_hoe2.json
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot; cc&quot;,
    &quot; / &quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_hoe&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_193240.png&quot; alt=&quot;铜锄1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_193235.png&quot; alt=&quot;铜锄2&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;铜剑&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_sword.json
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot; c &quot;,
    &quot; c &quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_sword&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_193615.png&quot; alt=&quot;铜剑&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;铜斧&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_axe.json
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot;cc &quot;,
    &quot;c/ &quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_axe&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// copper_axe2.json 第二种合成方式
{
  &quot;type&quot;: &quot;minecraft:crafting_shaped&quot;,
  &quot;pattern&quot;: [
    &quot; cc&quot;,
    &quot; /c&quot;,
    &quot; / &quot;,
  ],
  &quot;key&quot;: {
    &quot;c&quot;: {
      &quot;item&quot;: &quot;minecraft:copper_ingot&quot;,
    },
    &quot;/&quot;: {
      &quot;item&quot;: &quot;minecraft:stick&quot;,
    },
  },
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:copper_axe&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_192921.png&quot; alt=&quot;铜斧1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_192916.png&quot; alt=&quot;铜斧2&quot;&gt;&lt;/p&gt;
&lt;h3&gt;附魔&lt;/h3&gt;
&lt;p&gt;当这些自定义工具，放置到附魔台上时，发现无法进行附魔。查阅了一些资料发现，需要对这些工具打上附魔的&lt;code&gt;tag&lt;/code&gt;标签。
同样在&lt;code&gt;main\resources\data\minecraft\tags\item\enchantable&lt;/code&gt;文件夹下(文件夹不存在则新建),分别创建&lt;code&gt;mining.json&lt;/code&gt;和&lt;code&gt;sword.json&lt;/code&gt;文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// mining.json
{
  &quot;replace&quot;: false,
  &quot;values&quot;: [
    &quot;mymod:copper_axe&quot;,
    &quot;mymod:copper_hoe&quot;,
    &quot;mymod:copper_pickaxe&quot;,
    &quot;mymod:copper_shovel&quot;,
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// sword.json
{
  &quot;replace&quot;: false,
  &quot;values&quot;: [
    &quot;mymod:copper_sword&quot;,
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，放到附魔台上就可以正常附魔了。&lt;/p&gt;
&lt;h3&gt;实现效果&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250309_201138.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.DvrkaTa-.jpg"/><enclosure url="/_astro/thumbnail.DvrkaTa-.jpg"/></item><item><title>minecraft模组：01创建物品</title><link>https://saneko.me/blog/8e7bcacae687</link><guid isPermaLink="true">https://saneko.me/blog/8e7bcacae687</guid><description>本文简要介绍了如何使用Fabric模组在Minecraft中创建自定义物品。</description><pubDate>Sun, 02 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;p&gt;通过fabric编写模组，创建一个自定义物品&lt;code&gt;腐烂的苹果&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;合成：&lt;code&gt;腐肉&lt;/code&gt;+&lt;code&gt;苹果&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;效果：食用后，80%的概率&lt;code&gt;中毒&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;新建项目&lt;/h3&gt;
&lt;p&gt;选择&lt;strong&gt;Minecraft生成器&lt;/strong&gt;, 选择对应的minecraft版本，frabic版本及模组名称等&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250302_222639.png&quot; alt=&quot;创建项目&quot;&gt;&lt;/p&gt;
&lt;h3&gt;创建物品类&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;main&lt;/code&gt;中创建一个类&lt;code&gt;RottenApple&lt;/code&gt;来实现自定义物品&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class RottenApple{
    // 创建一个日志记录器
    public static final Logger LOGGER = LoggerFactory.getLogger(&quot;MyMod&quot;);

    // 定义一个静态的食物组件，用于表示腐烂的食物属性
    public static final FoodComponent ROTTEN_FOOD_COMPONENT = new FoodComponent.Builder()
            .alwaysEdible()
            // 添加中毒状态效果，持续 6 秒（120 游戏刻），等级为 0，触发概率为 80%
            .statusEffect(new StatusEffectInstance(StatusEffects.POISON, 6 * 20, 0), 0.8f)
            .build();

    // 用于在物品上添加自定义的提示信息
    static class CustomToolTip extends Item {
        static String tips;
        public CustomToolTip(Settings settings, String tip) {
            super(settings);
            tips = tip;
        }

        @Override
        public void appendTooltip(ItemStack stack, TooltipContext context, List&amp;#x3C;Text&gt; tooltip, TooltipType type) {
            tooltip.add(Text.translatable(tips).styled(s -&gt; s.withColor(0x6D4C41)));
        }
    }

    // 定义一个静态的腐烂苹果物品实例, 并通过 register 方法将该物品注册到游戏中
    public static final Item RottenApple = register(
            new CustomToolTip(new Item.Settings().food(ROTTEN_FOOD_COMPONENT), &quot;腐烂的&quot;),
            &quot;rotten_apple&quot;
    );

    // 静态方法，用于注册物品到游戏的物品注册表中
    public static Item register(Item item, String id) {
        // 创建物品的标识符
        Identifier itemID = Identifier.of(&quot;mymod&quot;, id);
        // 将物品注册到游戏的物品注册表中，并返回注册后的物品实例
        return Registry.register(Registries.ITEM, itemID, item);
    }

    // 初始化
    public static void initialize() {
        // 将物品添加到 FOOD_AND_DRINK 物品组中
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.FOOD_AND_DRINK)
                .register((itemGroup) -&gt; itemGroup.add(RottenApple));

        // 将腐烂苹果物品添加到堆肥机会注册表中，有 30% 的概率增加堆肥器的等级
        CompostingChanceRegistry.INSTANCE.add(RottenApple, 0.3f);

        LOGGER.info(&quot;[DEBUG] 物品注册完成&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;Mymod&lt;/code&gt;中进行模组初始化&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Mymod implements ModInitializer {

    @Override
    public void onInitialize() {
        RottenApple.initialize();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;gradle&lt;/code&gt;中，选择&lt;code&gt;fabric&lt;/code&gt;,运行&lt;code&gt;runClient&lt;/code&gt;，进入游戏。新建游戏后，在物品栏中可以看到我们新建的物品。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250302_232351.png&quot; alt=&quot;新建的物品&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是此时，物品名称和纹理都还没有添加&lt;/p&gt;
&lt;h3&gt;添加翻译&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;main\resources\assets\mymod\lang&lt;/code&gt;文件夹中，新建一个&lt;code&gt;en_us.json&lt;/code&gt;  (如果文件夹不存在则新建)&lt;/p&gt;
&lt;p&gt;写入翻译和名称&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;item.mymod.rotten_apple&quot;: &quot;RottenApple&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250302_232827.png&quot; alt=&quot;添加翻译&quot;&gt;&lt;/p&gt;
&lt;h3&gt;添加纹理和模型&lt;/h3&gt;
&lt;h4&gt;模型&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;main\resources\assets\mymod\models\item&lt;/code&gt;文件夹中，新建一个&lt;code&gt;rotten_apple.json&lt;/code&gt;(如果文件夹不存在则新建)&lt;/p&gt;
&lt;p&gt;写入模型数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;parent&quot;: &quot;item/generated&quot;,
  &quot;textures&quot;: {
    &quot;layer0&quot;: &quot;mymod:item/rotten_apple&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;parent&lt;/strong&gt;  模型要继承的父模型。大多物品继承的模型是 item/generate, 也有其他的，比如 item/handheld，用于拿在玩家手中的物品，例如工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;textures&lt;/strong&gt;  为模型定义纹理的地方。 layer0 是模型使用的纹理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;纹理&lt;/h4&gt;
&lt;p&gt;将纹理文件放在&lt;code&gt;main\resources\assets\mymod\textures\item&lt;/code&gt;文件夹中，并命名为&lt;code&gt;rotten_apple.png&lt;/code&gt;(如果文件夹不存在则新建)&lt;/p&gt;
&lt;p&gt;需要注意的是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须是png格式的文件&lt;/li&gt;
&lt;li&gt;像素需要是16×16或32×32&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250302_234104.png&quot; alt=&quot;添加模型和纹理&quot;&gt;&lt;/p&gt;
&lt;h3&gt;合成配方&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;腐肉&lt;/code&gt; + &lt;code&gt;苹果&lt;/code&gt; 进行无序合成&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;main\resources\data\mymod\recipe&lt;/code&gt;文件夹中，新建一个&lt;code&gt;rotten_apple.json&lt;/code&gt;(如果文件夹不存在则新建)&lt;/p&gt;
&lt;p&gt;写入合成配方(无序合成)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;type&quot;: &quot;minecraft:crafting_shapeless&quot;,
  &quot;ingredients&quot;: [
    {
      &quot;item&quot;: &quot;minecraft:rotten_flesh&quot;,
    },
    {
      &quot;item&quot;: &quot;minecraft:apple&quot;,
    },
  ],
  &quot;result&quot;: {
    &quot;id&quot;: &quot;mymod:rotten_apple&quot;,
    &quot;count&quot;: 1,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250302_234652.png&quot; alt=&quot;合成配方&quot;&gt;&lt;/p&gt;
&lt;h3&gt;实现效果&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250303_211828.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Cx-7bZfn.jpg"/><enclosure url="/_astro/thumbnail.Cx-7bZfn.jpg"/></item><item><title>minecraft铁傀儡生成机制</title><link>https://saneko.me/blog/37ac9073a69e</link><guid isPermaLink="true">https://saneko.me/blog/37ac9073a69e</guid><description>本文简要介绍了minecraft铁傀儡的生成机制，包括生成条件、恐慌状态下的生成、交谈状态下的生成、生成过程以及冷却时间等核心机制。</description><pubDate>Thu, 27 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;生成条件&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Java&lt;/code&gt;版里，不管村民是&lt;strong&gt;惊慌&lt;/strong&gt;还是&lt;strong&gt;交谈&lt;/strong&gt;，都能生成铁傀儡。&lt;/p&gt;
&lt;p&gt;村民尝试生成铁傀儡时，系统最多试十次，在以村民为中心，水平 &lt;strong&gt;±8&lt;/strong&gt; 格、垂直 &lt;strong&gt;±6&lt;/strong&gt; 格（17×13×17 格范围）内生成。&lt;/p&gt;
&lt;p&gt;每次尝试，系统会在该范围随机选个 1×1、高 13 格的柱状区域，从顶向下找合适位置生成铁傀儡，找不到就失败。若多个位置 X、Z 坐标一样，铁傀儡只在最高处生成，低处即便合适也不行 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250227_220604.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;铁傀儡的合适位置需符合以下条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该处需为空气或液体&lt;/li&gt;
&lt;li&gt;该处下方的方块必须是固体方块或细雪&lt;/li&gt;
&lt;li&gt;该处下方不能是下列方块：
&lt;ul&gt;
&lt;li&gt;冰&lt;/li&gt;
&lt;li&gt;浮冰&lt;/li&gt;
&lt;li&gt;玻璃（含染色与遮光变种）&lt;/li&gt;
&lt;li&gt;玻璃板（含染色变种）&lt;/li&gt;
&lt;li&gt;树叶&lt;/li&gt;
&lt;li&gt;仙人掌&lt;/li&gt;
&lt;li&gt;TNT&lt;/li&gt;
&lt;li&gt;蜘蛛网&lt;/li&gt;
&lt;li&gt;信标&lt;/li&gt;
&lt;li&gt;荧石&lt;/li&gt;
&lt;li&gt;海晶灯&lt;/li&gt;
&lt;li&gt;潮涌核心&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选定合适位置后，若该位置下方为可生成方块，且生成的铁傀儡不会和任何其他实体和方块的碰撞箱碰撞，则铁傀儡会被生成在此处；否则，此次尝试失败。&lt;/p&gt;
&lt;h3&gt;恐慌&lt;/h3&gt;
&lt;p&gt;村民在遇到&lt;strong&gt;敌对生物&lt;/strong&gt;时，会进入&lt;code&gt;惊慌&lt;/code&gt;状态。村民对敌对生物的&lt;strong&gt;感知距离&lt;/strong&gt;如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static final ImmutableMap&amp;#x3C;EntityType&amp;#x3C;?&gt;, Float&gt; SQUARED_DISTANCES_FOR_DANGER = ImmutableMap.&amp;#x3C;EntityType&amp;#x3C;?&gt;, Float&gt;builder()
    .put(EntityType.DROWNED, 8.0F)  // 溺尸：8格
    .put(EntityType.EVOKER, 12.0F)  // 唤魔者：12格
    .put(EntityType.HUSK, 8.0F)  // 尸壳：8格
    .put(EntityType.ILLUSIONER, 12.0F)  // 幻术师：12格
    .put(EntityType.PILLAGER, 15.0F)  // 掠夺者：15格
    .put(EntityType.RAVAGER, 12.0F)  // 劫掠兽：12格
    .put(EntityType.VEX, 8.0F)  // 恼鬼：8格
    .put(EntityType.VINDICATOR, 10.0F)  // 卫道士：10格
    .put(EntityType.ZOGLIN, 10.0F)  // 僵尸疣猪兽：10格
    .put(EntityType.ZOMBIE, 8.0F)  // 僵尸：8格
    .put(EntityType.ZOMBIE_VILLAGER, 8.0F)  // 僵尸村民：8格
    .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在惊慌状态的村民会每100游戏刻（5秒）尝试生成一次铁傀儡，若周围有2个有意愿生成铁傀儡的村民（一共3个村民），就会判定成功进行生成。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected void keepRunning(ServerWorld serverWorld, VillagerEntity villagerEntity, long l) {
    if (l % 100L == 0L) {
        villagerEntity.summonGolem(serverWorld, l, 3);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;交谈&lt;/h3&gt;
&lt;p&gt;如果村民不处于&lt;code&gt;惊慌&lt;/code&gt;状态，村民会每1200游戏刻（60秒）传播一次言论，若周围有4个村民同样有生成铁傀儡的意愿（一共5个村民），就会判定成功进行生成步骤。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void talkWithVillager(ServerWorld world, VillagerEntity villager, long time) {
    if ((time &amp;#x3C; this.gossipStartTime || time &gt;= this.gossipStartTime + 1200L) &amp;#x26;&amp;#x26; (time &amp;#x3C; villager.gossipStartTime || time &gt;= villager.gossipStartTime + 1200L)) {
        // 分享来自另一个村民的言论
        this.gossip.shareGossipFrom(villager.gossip, this.random, 10);
        this.gossipStartTime = time;
        villager.gossipStartTime = time;
        // 尝试在给定的世界中生成一个铁傀儡
        this.summonGolem(world, time, 5);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean canSummonGolem(long time) {
    // 如果最近没有睡觉且记忆中没有记录最近检测到铁傀儡，则可以召唤
    return !this.hasRecentlySlept(this.getWorld().getTime()) ? false : !this.brain.hasMemoryModule(MemoryModuleType.GOLEM_DETECTED_RECENTLY);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void summonGolem(ServerWorld world, long time, int requiredCount) {
    if (this.canSummonGolem(time)) {
        Box box = this.getBoundingBox().expand(10.0, 10.0, 10.0);
        // 在指定区域内查找所有村民实体
        List&amp;#x3C;VillagerEntity&gt; list = world.getNonSpectatingEntities(VillagerEntity.class, box);
        List&amp;#x3C;VillagerEntity&gt; list2 = list.stream().filter(villager -&gt; villager.canSummonGolem(time)).limit(5L).toList();
        // 尝试召唤铁傀儡
        if (list2.size() &gt;= requiredCount) {
            if (!LargeEntitySpawnHelper.trySpawnAt(
                    EntityType.IRON_GOLEM, SpawnReason.MOB_SUMMONED, world, this.getBlockPos(), 10, 8, 6, LargeEntitySpawnHelper.Requirements.IRON_GOLEM, false
                )
                .isEmpty()) {
                // 遍历所有村民并记录他们看到铁傀儡的时间
                list.forEach(GolemLastSeenSensor::rememberIronGolem);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;冷却&lt;/h3&gt;
&lt;p&gt;当铁傀儡生成之后，以该村民为中心周围10格内的所有村民会进入600游戏刻（30秒）的冷却时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 让实体记住它最近看到了铁傀儡的方法
public static void rememberIronGolem(LivingEntity entity) {
    // 设置“Golem Detected Recently”的记忆为true，并设置过期时间为599毫秒
    entity.getBrain().remember(MemoryModuleType.GOLEM_DETECTED_RECENTLY, true, 599L);
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.DGTXbCjm.jpg"/><enclosure url="/_astro/thumbnail.DGTXbCjm.jpg"/></item><item><title>minecraft刷怪机制</title><link>https://saneko.me/blog/1c77a76798e1</link><guid isPermaLink="true">https://saneko.me/blog/1c77a76798e1</guid><description>本文简要介绍了Minecraft的刷怪机制，包括生物类别划分、生成上限计算、核心生成条件、生成步骤流程以及LC值对刷怪效率的影响。</description><pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;生物类别&lt;/h3&gt;
&lt;p&gt;minecraft中生物被划分为8种类别，分别为&lt;code&gt;怪物（Monster）&lt;/code&gt;、&lt;code&gt;动物（Creature）&lt;/code&gt;、&lt;code&gt;环境生物（Ambient）&lt;/code&gt;、&lt;code&gt;美西螈（Axolotls）&lt;/code&gt;、&lt;code&gt;地下水生生物（Underground Water Creature）&lt;/code&gt;、&lt;code&gt;水生生物（Water Creature）&lt;/code&gt;、&lt;code&gt;水下环境生物（Water Ambient）&lt;/code&gt;和&lt;code&gt;其他（Misc）&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| 类别         | 上限乘数 | 友好生物 | 自动清除 | 清除半径 |
| ------------ | -------- | -------- | -------- | -------- |
| 怪物         | 70       | 否       | 是       | 128      |
| 动物         | 10       | 是       | 否       | 128      |
| 环境生物     | 15       | 是       | 是       | 128      |
| 美西螈       | 5        | 是       | 是       | 128      |
| 地下水生生物 | 5        | 是       | 是       | 128      |
| 水生生物     | 5        | 是       | 是       | 128      |
| 水下环境生物 | 20       | 是       | 是       | 64       |
| 其他         | -1       | 是       | 否       | 128      |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public enum SpawnGroup implements StringIdentifiable {
	MONSTER(&quot;monster&quot;, 70, false, false, 128),
	CREATURE(&quot;creature&quot;, 10, true, true, 128),
	AMBIENT(&quot;ambient&quot;, 15, true, false, 128),
	AXOLOTLS(&quot;axolotls&quot;, 5, true, false, 128),
	UNDERGROUND_WATER_CREATURE(&quot;underground_water_creature&quot;, 5, true, false, 128),
	WATER_CREATURE(&quot;water_creature&quot;, 5, true, false, 128),
	WATER_AMBIENT(&quot;water_ambient&quot;, 20, true, false, 64),
	MISC(&quot;misc&quot;, -1, true, true, 128);
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成上限&lt;/h3&gt;
&lt;p&gt;为了阻止生物无限生成，每个生物类别都有它们的生成上限数量。生成上限数量不仅与类别本身有关，也与玩家有关。&lt;/p&gt;
&lt;p&gt;以每个玩家所在区块为中心的17×17区块都被认为是&lt;code&gt;可生成区块&lt;/code&gt;，而每个生物类别的生成上限&lt;em&gt;m&lt;/em&gt;是可生成区块数量&lt;em&gt;c&lt;/em&gt;、生物类别上限乘数&lt;em&gt;a&lt;/em&gt;之积除以289得到的，即m=ac/289。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当仅有一位玩家时生成上限m就等于上线乘数a，即m=a&lt;/li&gt;
&lt;li&gt;当存在n名玩家时假如所有玩家所在的可生成区块互不干扰，则生成上限为最大值na，假如所有玩家在同一个区块，则生成上限与一名玩家相同为a。因此该情况下，生成上限m在[a, na]范围之间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;生成条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;光照条件&lt;/strong&gt;：天空光小于等于7，方块光等于0。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;空间要求&lt;/strong&gt;：下方必须是非透明方块。僵尸/骷髅需要至少2格高，蜘蛛需要至少3格宽的空间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;玩家距离&lt;/strong&gt;：距离玩家半径24~128的球形区域。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生成上限&lt;/strong&gt;：超过生成上限时，将生成失败。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;碰撞箱检测&lt;/strong&gt;： 生成时需满足碰撞箱无遮挡。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;核心步骤&lt;/h3&gt;
&lt;h4&gt;1. 生物群系筛选&lt;/h4&gt;
&lt;p&gt;根据生物群系配置获取可生成的生物列表，并基于群系生成概率决定是否进入生成循环。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SpawnSettings spawnSettings = biomeEntry.value().getSpawnSettings();
Pool&amp;#x3C;SpawnSettings.SpawnEntry&gt; pool = spawnSettings.getSpawnEntries(SpawnGroup.CREATURE);
if (!pool.isEmpty()) {
    int i = chunkPos.getStartX();
    int j = chunkPos.getStartZ();

    while (random.nextFloat() &amp;#x3C; spawnSettings.getCreatureSpawnProbability()) {
    // ...
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 初始点&lt;/h4&gt;
&lt;p&gt;X/Z轴在区块内随机选择初始点，Y轴从非透明方块的最高点+1到世界底部的随机值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static BlockPos getRandomPosInChunkSection(World world, WorldChunk chunk) {
    ChunkPos chunkPos = chunk.getPos();
    int i = chunkPos.getStartX() + world.random.nextInt(16);
    int j = chunkPos.getStartZ() + world.random.nextInt(16);
    int k = chunk.sampleHeightmap(Heightmap.Type.WORLD_SURFACE, i, j) + 1;
    int l = MathHelper.nextBetween(world.random, world.getBottomY(), k);
    return new BlockPos(i, l, j);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 生成尝试&lt;/h4&gt;
&lt;p&gt;每个初始点触发3次生成游走尝试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; // 外层循环：3次生成游走尝试
for (int k = 0; k &amp;#x3C; 3; k++) {
    int l = pos.getX(); // 初始X坐标
    int m = pos.getZ(); // 初始Z坐标
    SpawnSettings.SpawnEntry spawnEntry = null; // 当前生成的生物配置
    EntityData entityData = null; // 生物初始化数据
    // 计算本次游走的生成尝试次数（1-4次）
    int o = MathHelper.ceil(world.random.nextFloat() * 4.0F);
    int p = 0; // 本次游走已生成的生物数

    // 内层循环：单次游走中的生成尝试
    for (int q = 0; q &amp;#x3C; o; q++) {
        // 随机偏移坐标（X/Z各±0-5格）
        l += world.random.nextInt(6) - world.random.nextInt(6);
        m += world.random.nextInt(6) - world.random.nextInt(6);
        mutable.set(l, i, m); // 设置候选坐标

        // 计算精确坐标（用于距离检查）
        double d = (double)l + 0.5;
        double e = (double)m + 0.5;
        // 查找最近玩家（用于距离判定）
        PlayerEntity playerEntity = world.getClosestPlayer(d, (double)i, e, -1.0, false);

        if (playerEntity != null) {
            // 计算与玩家的平方距离（优化计算）
            double f = playerEntity.squaredDistanceTo(d, (double)i, e);

            // 检查候选位置是否合法
            if (isAcceptableSpawnPosition(world, chunk, mutable, f)) {
                // 首次生成时选择生物类型
                if (spawnEntry == null) {
                    // 根据群系/结构选择可生成的生物条目
                    Optional&amp;#x3C;SpawnSettings.SpawnEntry&gt; optional = pickRandomSpawnEntry(
                        world, structureAccessor, chunkGenerator, group, world.random, mutable
                    );
                    if (optional.isEmpty()) break; // 无可用生物则终止本次游走

                    spawnEntry = optional.get();
                    // 动态调整生成次数（基于生物群大小）
                    o = spawnEntry.minGroupSize + world.random.nextInt(
                        1 + spawnEntry.maxGroupSize - spawnEntry.minGroupSize
                    );
                }

                // 最终生成条件验证
                if (canSpawn(world, group, structureAccessor, chunkGenerator, spawnEntry, mutable, f)
                    &amp;#x26;&amp;#x26; checker.test(spawnEntry.type, mutable, chunk)) { // 自定义条件检查

                    // 创建生物实体
                    MobEntity mobEntity = createMob(world, spawnEntry.type);
                    if (mobEntity == null) return; // 创建失败终止方法

                    // 设置生物位置和角度
                    mobEntity.refreshPositionAndAngles(d, (double)i, e,
                        world.random.nextFloat() * 360.0F, 0.0F);

                    // 验证生物生成合法性
                    if (isValidSpawn(world, mobEntity, f)) {
                        // 初始化生物（生成装备/设置属性）
                        entityData = mobEntity.initialize(world,
                            world.getLocalDifficulty(mobEntity.getBlockPos()),
                            SpawnReason.NATURAL,
                            entityData
                        );

                        // 更新计数器
                        j++;
                        p++;

                        // 将生物加入世界
                        world.spawnEntityAndPassengers(mobEntity);
                        runner.run(mobEntity, chunk); // 执行生成后回调

                        // 检查区块生物上限
                        if (j &gt;= mobEntity.getLimitPerChunk()) {
                            return; // 达到上限立即终止
                        }

                        // 检查单次尝试生成数限制（如末影人最多生成1只）
                        if (mobEntity.spawnsTooManyForEachTry(p)) {
                            break;
                        }
                    }
                }
            }
        }
    } // 结束内层循环
} // 结束外层循环
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;生成验证&lt;/h4&gt;
&lt;p&gt;满足以下条件，将导致生成失败&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;玩家距离过近或过远&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光照条件不符合&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间不足&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生物群系或结构不匹配&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密度上限已满&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;碰撞箱被遮挡&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生物类型不可生成&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;LC值对刷怪的影响&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LC&lt;/strong&gt;(Loaded Chunk)，即玩家所在区块的最大渲染区段高度，一般是16的倍数减1。
在核心步骤中的第2步，选择随机初始点时，会随机在最高的非透明方块所在位置+1与世界底部之间取一个值，作为刷怪初始点的高度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 LC 值低时，意味着最高遮光方块的纵坐标较低，刷怪初始点可选择的高度范围小，就有更大的概率选到合适的刷怪高度，从而提高刷怪成功率。&lt;/li&gt;
&lt;li&gt;若 LC 值高，比如刷怪平台建在 y=100 处，那么 y 轴坐标可能是 0-101 之间的任何一个数，选到刚好能刷怪高度的概率就低，刷怪效率自然下降。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/thumbnail.C4hVqvoM.jpg"/><enclosure url="/_astro/thumbnail.C4hVqvoM.jpg"/></item><item><title>minecraft药水酿造</title><link>https://saneko.me/blog/2b110afc8045</link><guid isPermaLink="true">https://saneko.me/blog/2b110afc8045</guid><description>本文简要介绍了Minecraft中药水酿造的完整系统，包括药水分类、酿造设备、制作流程、配方表以及相关游戏机制。</description><pubDate>Sun, 23 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;药水&lt;/h3&gt;
&lt;p&gt;minecraft药水分为&lt;strong&gt;基础药水&lt;/strong&gt;（无效果，如粗制药水）、&lt;strong&gt;正面效果药水&lt;/strong&gt;（如力量、夜视）、&lt;strong&gt;负面效果药水&lt;/strong&gt;（如虚弱、剧毒）和&lt;strong&gt;混合效果药水&lt;/strong&gt;（如隐身）。部分药水还可通过添加火药或龙息制成&lt;strong&gt;喷溅型&lt;/strong&gt;或&lt;strong&gt;滞留型&lt;/strong&gt;，实现群体效果。&lt;/p&gt;
&lt;h3&gt;酿造设备&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;酿造台&lt;/strong&gt;：用于添加材料到玻璃瓶里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;炼药锅&lt;/strong&gt;：用于储存水，可以装满3个玻璃瓶。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;烈焰粉&lt;/strong&gt;：酿造台的燃料。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;玻璃瓶&lt;/strong&gt;：所有药水的容器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;水瓶&lt;/strong&gt;：所有药水的基础。将一个玻璃瓶从水源或是炼药锅里装水获得。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;酿造流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;制作水瓶（玻璃瓶装水）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入地狱疣酿造&lt;strong&gt;粗制药水&lt;/strong&gt;（大多数药水的基础）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加特定材料（如糖→速度药水、金萝卜→夜视药水）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可选：用萤石增强效果或红石延长持续时间。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_224154.png&quot; alt=&quot;酿造配方&quot;&gt;&lt;/p&gt;
&lt;h3&gt;机制&lt;/h3&gt;
&lt;h4&gt;叠加&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;同种药水效果不可叠加，以最高等级或剩余时间覆盖。&lt;/li&gt;
&lt;li&gt;部分效果（如抗火、缓降）可与其他增益共存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;投掷&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;喷溅型药水垂直向上扔出可保留完整效果时间，若随意投掷可能缩短效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;亡灵生物&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;亡灵生物（如僵尸）受治疗药水伤害，但被伤害药水治疗。剧毒和再生药水对其无效。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/thumbnail.AqXt5gez.jpg"/><enclosure url="/_astro/thumbnail.AqXt5gez.jpg"/></item><item><title>minecraft村民交易</title><link>https://saneko.me/blog/d39dd7a2884b</link><guid isPermaLink="true">https://saneko.me/blog/d39dd7a2884b</guid><description>本文简要介绍了minecraft中村民交易系统的完整机制，包括职业分类、交易规则和各职业具体交易内容。</description><pubDate>Fri, 21 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;交易基础&lt;/h3&gt;
&lt;p&gt;minecraft村民共有13种职业（如农民、图书管理员、盔甲匠等），每种职业对应特定&lt;strong&gt;工作站点&lt;/strong&gt;（如讲台对应图书管理员）。失业村民会绑定未被占用的工作站点转职，但傻子村民（穿绿袍）无法交易。&lt;/p&gt;
&lt;h3&gt;交易机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;工作站点: 村民共有13种职业，每种对应特定工作站点方块（如讲台→图书管理员，高炉→盔甲匠）。失业村民会绑定附近未被占用的工作站转职，但傻子村民（绿袍）无法交易&lt;/li&gt;
&lt;li&gt;交易层级: 交易分新手、学徒、老手、专家、大师共5个等级，需完成当前层级交易才能解锁下一层级选项&lt;/li&gt;
&lt;li&gt;补货规则: 交易次数耗尽后，村民需接触工作站和床补货，每天最多补货2次&lt;/li&gt;
&lt;li&gt;价格波动: 频繁交易同一物品会暂时涨价，补货后价格重置。治愈被僵尸感染的村民或获得村庄英雄效果时可极大降低交易价格&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;交易内容&lt;/h3&gt;
&lt;h4&gt;盔甲商[高炉]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_191657.png&quot; alt=&quot;高炉&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_193315.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;屠夫[烟熏炉]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_193740.png&quot; alt=&quot;烟熏炉&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_194034.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;制图师[制图台]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_195055.png&quot; alt=&quot;制图台&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_195200.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;牧师[酿造台]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_195845.png&quot; alt=&quot;酿造台&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250222_195930.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;农民[堆肥桶]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_124401.png&quot; alt=&quot;堆肥桶&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_124540.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;渔夫[木桶]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_125002.png&quot; alt=&quot;木桶&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_125105.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;制箭师[制箭台]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_125423.png&quot; alt=&quot;制箭台&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_125540.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;皮匠[炼药锅]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_130042.png&quot; alt=&quot;炼药锅&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_130125.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;图书管理员[讲台]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_130734.png&quot; alt=&quot;讲台&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_130822.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;石匠[切石机]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_131141.png&quot; alt=&quot;切石机&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_131234.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_131335.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;牧羊人[织布机]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_131758.png&quot; alt=&quot;织布机&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_131914.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_132006.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;工具匠[锻造台]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_132829.png&quot; alt=&quot;锻造台&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_132721.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;武器匠[砂轮]&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250825_185822.png&quot; alt=&quot;砂轮&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250223_135848.png&quot; alt=&quot;交易列表&quot;&gt;&lt;/p&gt;
&lt;h4&gt;失业&lt;/h4&gt;
&lt;p&gt;没有穿着职业着装，只有对应生物群系的衣服的村民为失业村民，它们无法进行交易。&lt;/p&gt;
&lt;h4&gt;傻子&lt;/h4&gt;
&lt;p&gt;傻子是穿着绿色袍子的村民。自然生成的傻子不会提供任何交易。与失业村民不同的是，傻子不能获得以及改变职业。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.BfZLA7f7.jpg"/><enclosure url="/_astro/thumbnail.BfZLA7f7.jpg"/></item><item><title>生成minecraft源码</title><link>https://saneko.me/blog/1b9102e33b56</link><guid isPermaLink="true">https://saneko.me/blog/1b9102e33b56</guid><description>本文简要介绍了利用Fabric工具链生成Minecraft可读化源码的原理与流程。</description><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;说明&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Minecraft的官方源码始终未公开&lt;/strong&gt;，其Java版客户端通过&lt;strong&gt;混淆技术&lt;/strong&gt;对类名、方法名进行随机化处理（如a()、b()等无意义命名）。而通过Fabric生成的源码本质是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;反编译产物：基于Mojang发布的JAR包，使用CFR反编译器生成&lt;/li&gt;
&lt;li&gt;社区映射：通过Yarn项目（社区维护的映射表）将混淆名称转为语义化命名（如method_1234→calculateBlockDamage）&lt;/li&gt;
&lt;li&gt;受限结构：仅包含游戏运行时核心逻辑（如net.minecraft.block包），不包含渲染引擎等闭源模块&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生成的源码&lt;strong&gt;不可二次分发&lt;/strong&gt;（违反Mojang EULA第2节）&lt;/li&gt;
&lt;li&gt;生成的源码与Minecraft版本&lt;strong&gt;严格绑定&lt;/strong&gt;（如1.20.1生成的类无法直接用于1.21开发）&lt;/li&gt;
&lt;li&gt;禁止直接修改生成的源码（修改无效，需通过Mixin或API注入）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;环境准备&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装&lt;a href=&quot;https://adoptium.net/temurin/releases/&quot;&gt;JDK 17+&lt;/a&gt;{hugeicons:java}&lt;/li&gt;
&lt;li&gt;安装&lt;a href=&quot;https://www.jetbrains.com/idea/download/?section=windows&quot;&gt;IntelliJ IDEA&lt;/a&gt;{devicon:intellij}&lt;/li&gt;
&lt;li&gt;安装插件Minecraft Development（自动配置Fabric）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;生成步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;下载&lt;a href=&quot;https://fabricmc.net/develop/template/&quot;&gt;fabric模组模板&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;修改Gradle配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;# settings.gradle 添加镜像源
repositories {
    maven { url &apos;https://maven.aliyun.com/nexus/content/groups/public&apos; }
    maven { url &apos;https://repository.hanbings.io/proxy&apos; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;执行生成命令&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 控制台运行
./gradlew genSources
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证生成结果&lt;/h3&gt;
&lt;p&gt;在IDEA中的&lt;code&gt;外部库&lt;/code&gt;中可以查看到&lt;code&gt;net.minecraft:minecraft-&amp;#x3C;版本&gt;@merged-named&lt;/code&gt;
展开net.minecraft包查看反编译后的Yarn映射源码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250220_230534.png&quot; alt=&quot;源码映射&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.DcrKxY69.jpg"/><enclosure url="/_astro/thumbnail.DcrKxY69.jpg"/></item><item><title>本地部署deepseek-r1</title><link>https://saneko.me/blog/149181eaf3aa</link><guid isPermaLink="true">https://saneko.me/blog/149181eaf3aa</guid><description>本文介绍本地部署deepseek-r1模型的流程。</description><pubDate>Tue, 18 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Card } from &apos;astro-pure/user&apos;
import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;模型参数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;deepseek-r1&lt;/code&gt;模型不同参数的特点，适用场景及硬件配置如下，根据自己实际情况选择对应版本。&lt;/p&gt;
&lt;p&gt;| DeepSeek 模型版本    | 参数量 | 特点                             | 适用场景                                                     | 推荐硬件配置                                                 |
| -------------------- | ------ | -------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| DeepSeek - R1 - 1.5B | 1.5B   | 轻量级，参数量与规模小           | 短文本生成、基础问答等轻量级任务                             | 4 核处理器、8G 内存，无需显卡                                |
| DeepSeek - R1 - 7B   | 7B     | 性能与硬件需求较平衡             | 文案撰写、表格处理、统计分析等中等复杂度任务                 | 8 核处理器、16G 内存，Ryzen 7 或更高 CPU，RTX 3060（12GB）或更高显卡 |
| DeepSeek - R1 - 8B   | 8B     | 性能略优于 7B 模型               | 代码生成、逻辑推理等高精度轻量级任务                         | 8 核处理器、16G 内存，Ryzen 7 或更高 CPU，RTX 3060（12GB）或 4060 显卡 |
| DeepSeek - R1 - 14B  | 14B    | 高性能，擅长复杂任务             | 长文本生成、数据分析等复杂任务                               | i9 - 13900K 或更高 CPU、32G 内存，RTX 4090（24GB）或 A5000 显卡 |
| DeepSeek - R1 - 32B  | 32B    | 专业级，性能强大                 | 语言建模、大规模训练、金融预测等超大规模任务                 | Xeon 8 核（80GB）或更高配置                                  |
| DeepSeek - R1 - 70B  | 70B    | 顶级性能，适合大规模与高复杂计算 | 多模态任务预处理等高精度专业领域任务，适用于预算充足的企业或研究机构 | Xeon 8 核、128GB 内存或更高，8 张 A100/H100（80GB）或更高    |
| DeepSeek - R1 - 671B | 671B   | 超大规模，性能卓越、推理快       | 气候建模、基因组分析等国家级 / 超大规模 AI 研究及通用人工智能探索 | 64 核、512GB 或更高，8 张 A100/H100                          |&lt;/p&gt;
&lt;p&gt;&amp;#x3C;Card
as=&apos;a&apos;
href=&apos;https://zhuanlan.zhihu.com/p/22524204610&apos;
heading=&apos;百亿云互联&apos;
subheading=&apos;DeepSeek-R1模型1.5B/7B/14B/70B/671B区别及硬件配置要求&apos;
date=&apos;August 2021&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;本人部署的是8b版本，可以做到流畅推理。&lt;/p&gt;
&lt;h3&gt;部署&lt;/h3&gt;
&lt;h4&gt;安装ollama&lt;/h4&gt;
&lt;p&gt;进入&lt;a href=&quot;https://ollama.com/&quot;&gt;ollama&lt;/a&gt;官网，点击download后，选择对应电脑系统进行下载安装&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_173055.png&quot; alt=&quot;下载Ollama&quot;&gt;&lt;/p&gt;
&lt;h4&gt;下载模型&lt;/h4&gt;
&lt;p&gt;回到&lt;a href=&quot;https://ollama.com/&quot;&gt;ollama&lt;/a&gt;官网，在搜索栏中搜索&lt;code&gt;deepseek-r1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_174912.png&quot; alt=&quot;搜索deepseek&quot;&gt;&lt;/p&gt;
&lt;p&gt;选择对应参数模型，复制指令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_182347.png&quot; alt=&quot;下载模型&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里选择8b的模型，以管理员身份打开终端，运行复制的指令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ollama run deepseek-r1:8b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待模型下载安装完成。后续在终端中执行刚才的指令，就可以调用该模型来推理了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_182853.png&quot; alt=&quot;模型推理&quot;&gt;&lt;/p&gt;
&lt;h3&gt;可视化&lt;/h3&gt;
&lt;h4&gt;安装chatbox&lt;/h4&gt;
&lt;p&gt;进入&lt;a href=&quot;https://chatboxai.app/zh&quot;&gt;Chatbox&lt;/a&gt;官网，点击下载后，选择对应电脑系统进行下载安装&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_183550.png&quot; alt=&quot;下载Chatbox&quot;&gt;&lt;/p&gt;
&lt;h4&gt;模型设置&lt;/h4&gt;
&lt;p&gt;打开安装好后的&lt;strong&gt;chatbox&lt;/strong&gt;，点击设置，将模型提供方选择为&lt;strong&gt;OLLAMA API&lt;/strong&gt;，模型中会自动加载在ollama中下载部署好的模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_183753.png&quot; alt=&quot;模型设置&quot;&gt;&lt;/p&gt;
&lt;h4&gt;使用&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250218_185302.png&quot; alt=&quot;模型对话&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.C4Kg_LA9.jpg"/><enclosure url="/_astro/thumbnail.C4Kg_LA9.jpg"/></item><item><title>WPF依赖属性的注册与绑定</title><link>https://saneko.me/blog/e7470f56d9f2</link><guid isPermaLink="true">https://saneko.me/blog/e7470f56d9f2</guid><description>本文介绍WPF依赖属性的注册与绑定方法。</description><pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;说明&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;依赖属性&lt;/strong&gt;是 WPF 中的一种特殊属性，它扩展了传统的 .NET 属性，提供了更多功能，如数据绑定、动画、样式和资源支持。依赖属性的值不存储在对象本身，而是由 WPF 属性系统管理，这使得它们能够支持继承、默认值和值优先级等特性。&lt;/p&gt;
&lt;h3&gt;和普通属性区别&lt;/h3&gt;
&lt;p&gt;|              | &lt;strong&gt;普通属性&lt;/strong&gt;                             | &lt;strong&gt;依赖属性&lt;/strong&gt;                                      |
| ------------ | ---------------------------------------- | ------------------------------------------------- |
| &lt;strong&gt;存储方式&lt;/strong&gt; | 值直接存储在类的字段中                   | 值由 WPF 属性系统管理，存储在全局的依赖属性系统中 |
| &lt;strong&gt;功能支持&lt;/strong&gt; | 功能有限，不支持数据绑定、动画等高级特性 | 支持数据绑定、动画、样式、资源等高级功能          |
| &lt;strong&gt;值优先级&lt;/strong&gt; | 只有一个值来源                           | 支持多个值来源，并根据优先级决定最终值            |&lt;/p&gt;
&lt;h3&gt;注册&lt;/h3&gt;
&lt;p&gt;依赖属性通过 &lt;code&gt;DependencyProperty.Register&lt;/code&gt; 方法注册。以下是一个简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public class LineChart : Control
{
    public static readonly DependencyProperty ValuesProperty =
        DependencyProperty.Register(
            &quot;Values&quot;,     // 属性名称
            typeof(ObservableCollection&amp;#x3C;double&gt;),   // 属性类型
            typeof(LineChart), // 所属类型
            new PropertyMetadata(null, OnValuesChanged)); // 默认值，添加回调方法

    public ObservableCollection&amp;#x3C;double&gt; Values
    {
        get { return (ObservableCollection&amp;#x3C;double&gt;)GetValue(ValuesProperty); }
        set { SetValue(ValuesProperty, value); }
    }

    private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
       // 回调方法
       var chart = (LineChart)d;
        var newValues = (ObservableCollection&amp;#x3C;double&gt;)e.NewValue;
        // 更新 Series 的 Values
        if (chart.Series is LineSeries&amp;#x3C;double&gt; series)
        {
            series.Values = newValues;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;绑定&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;livecharts:LineChart Values=&quot;{Binding Values}&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中&lt;code&gt;LineChart&lt;/code&gt;控件的&lt;code&gt;Values&lt;/code&gt; 绑定到 &lt;code&gt;DataContext&lt;/code&gt; 中的 &lt;code&gt;Values&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖属性&lt;/strong&gt;是 WPF 中用于支持高级功能的特殊属性。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;DependencyProperty.Register&lt;/code&gt; 方法注册依赖属性。&lt;/li&gt;
&lt;li&gt;依赖属性支持数据绑定、动画、样式等高级功能。&lt;/li&gt;
&lt;li&gt;依赖属性的值由 WPF 属性系统管理，支持多个值来源和优先级。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过依赖属性，WPF 提供了强大的 UI 开发能力，使得开发者能够更灵活地构建复杂的用户界面。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.DVm-Q2pQ.jpg"/><enclosure url="/_astro/thumbnail.DVm-Q2pQ.jpg"/></item><item><title>CSharp资源释放管理</title><link>https://saneko.me/blog/d812cae7a99f</link><guid isPermaLink="true">https://saneko.me/blog/d812cae7a99f</guid><description>本文介绍如何通过实现IDisposable接口规范释放非托管资源，避免资源泄漏，并附C#代码示例和用法说明。</description><pubDate>Sat, 08 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在 .NET  中，&lt;code&gt;IDisposable&lt;/code&gt;接口是一个非常重要的接口，它主要用于管理非托管资源的释放。非托管资源是指那些不受 .NET 垃圾回收器（GC）管理的资源，例如&lt;code&gt;文件句柄&lt;/code&gt;、&lt;code&gt;数据库连接&lt;/code&gt;、&lt;code&gt;网络套接字&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;当你使用完这些资源后，需要及时释放它们以避免资源泄漏。&lt;code&gt;IDisposable&lt;/code&gt;接口提供了一种标准的方式来实现资源的释放逻辑。&lt;/p&gt;
&lt;h2&gt;举例&lt;/h2&gt;
&lt;h3&gt;实现接口&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;/// &amp;#x3C;summary&gt;
/// YOLOv8 目标检测预测器，提供基于ONNX模型的物体检测功能
/// &amp;#x3C;/summary&gt;
public class YoloV8Predictor: IDisposable
{
    private readonly Yolo yolo;   // yolo预测器

    public YoloV8Predictor()
    {
        // 初始化预测器
    }

    public void Detect()
    {
        // ... 目标检测
    }
    private bool disposedValue;
    // 受保护的虚方法，用于释放资源
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // 释放托管资源
                yolo.Dispose();
            }
            // 释放非托管资源
            disposedValue = true;
        }
    }
	// 实现 IDisposable 接口的 Dispose 方法
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
     // 终结器，用于在对象被垃圾回收时释放资源
    ~YoloV8Predictor()
    {
        Dispose(false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Dispose&lt;/code&gt; 方法调用了受保护的虚方法 &lt;code&gt;Dispose(bool disposing)&lt;/code&gt;，根据 &lt;code&gt;disposedValue&lt;/code&gt; 参数的值来决定是否释放托管资源。&lt;/li&gt;
&lt;li&gt;提供了一个终结器 &lt;code&gt;~YoloV8Predictor()&lt;/code&gt;，用于在对象被垃圾回收时释放非托管资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;调用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using YoloV8Predictor yolov8 = new();  // 使用 using 语句确保资源被正确释放
yolov8.Detect();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IDisposable&lt;/code&gt; 接口为管理非托管资源提供了一种标准的方式，通过实现该接口并在 &lt;code&gt;Dispose&lt;/code&gt; 方法中编写释放资源的代码，可以确保资源在不再使用时被及时释放，避免资源泄漏。同时，使用 &lt;code&gt;using&lt;/code&gt; 语句可以简化资源的管理，确保资源在作用域结束时自动释放。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.TH3a9OId.jpg"/><enclosure url="/_astro/thumbnail.TH3a9OId.jpg"/></item><item><title>CSharp使用uiautomator</title><link>https://saneko.me/blog/61a998371702</link><guid isPermaLink="true">https://saneko.me/blog/61a998371702</guid><description>本文介绍如何在C#中实现类似Android uiautomator的自动化操作，支持控制设备点击、输入、滑动等常用功能。</description><pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;
import { GithubCard } from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;h2&gt;开发背景&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uiautomator&lt;/code&gt;是Android平台上用于UI自动化测试的框架，可模拟用户对设备屏幕的各种操作，如点击、输入、滑动等。&lt;/p&gt;
&lt;p&gt;Python中有相对成熟的解决方案，普遍应用于自动化测试中。
项目地址：&lt;/p&gt;
&lt;p&gt;然而，近期在C#项目的开发进程中，我始终未能找到合适的解决方案。基于此状况，我计划借鉴该项目，着手打造一个C#版本的项目。&lt;/p&gt;
&lt;h2&gt;原理研究&lt;/h2&gt;
&lt;p&gt;首先先了解一下，该项目实现UIAutomator的原理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232731.png&quot; alt=&quot;实现原理&quot;&gt;&lt;/p&gt;
&lt;p&gt;简单来说，其实现原理是：在手机内开启一个rpc服务，然后PC端借助&lt;strong&gt;adb forward&lt;/strong&gt;（adb转发）将该服务在手机中的端口转发至本地。随后，PC端向此服务发送诸如点击、输入之类的请求，手机中的该服务便会执行相应操作来完成这些请求。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;尝试一下&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;通过以下指令，将U2.jar导入到设备中并启动一个端口固定为9008的服务端&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb push u2.jar /data/local/tmp
adb shell &quot;CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过adb的端口转发功能，随机一个端口与9008形成映射&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb forward tcp:1234 tcp:9008
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时python给本地的1234端口发送信息, 设备中的服务端也能同时接收到。&lt;/p&gt;
&lt;p&gt;但是，还需要了解发送的是什么信息。通过修改python的uiautomator2源码(如下图所示),打印相关信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232625.png&quot; alt=&quot;修改源码&quot;&gt;&lt;/p&gt;
&lt;p&gt;执行下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import uiautomator2 as u2
d = u2.connect()
d.press(&quot;home&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;method: GET
url: http://127.0.0.1:1234/ping
data: None
return: b&apos;pong&apos;
=================
method: POST
url: http://127.0.0.1:1234/jsonrpc/0
data: {&apos;jsonrpc&apos;: &apos;2.0&apos;, &apos;id&apos;: 1, &apos;method&apos;: &apos;pressKey&apos;, &apos;params&apos;: (&apos;home&apos;,)}
return: b&apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;result&quot;:true}\n&apos;
=================
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以推测Get请求只是用来获取服务端的状态，具体的执行是需要发送Post请求的。关键则在于post请求中的data数据。&lt;/p&gt;
&lt;p&gt;因此，只要C#也发送相同的请求，理论上也可以实现同样的效果。&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;/// &amp;#x3C;summary&gt; 用于启动u2.jar服务并在后台挂载 &amp;#x3C;/summary&gt;
public class MockAdbProcess
{
    private static readonly object _lock = new object();
    private Process _process;
    private string _serial;

    public string Serial
    {
        get { return _serial; }
        set { _serial = value; }
    }

    public MockAdbProcess(string serial)
    {
        Serial = serial;
        StartUiautomatorServer();
        AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
    }

    private void StartUiautomatorServer()
    {
        StopUiautomatorServer();
        string cmd = $&quot;adb -s {Serial} shell \&quot;CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main\&quot;&quot;;
        ProcessStartInfo startInfo = new()
        {
            FileName = &quot;cmd.exe&quot;,
            Arguments = $&quot;/c {cmd}&quot;,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden
        };

        Log.Info($&quot;launch uiautomator with cmd: {cmd}&quot;);
        _process = new Process { StartInfo = startInfo };
        _process.Start();

        // 读取错误信息
        _process.ErrorDataReceived += (sender, e) =&gt;
        {
            if(e.Data==null)
            if(e.Data != null &amp;#x26;&amp;#x26; e.Data.Contains(&quot;UiAutomation not connected&quot;))
            {
                Log.Error(&quot;UiAutomation not connected.&quot;);
            }
        };
        _process.BeginErrorReadLine();
    }

    public void StopUiautomatorServer()
    {
        string input = NProcess.RunReturnString($&quot;adb -s {Serial} shell ps -A -ef|findstr com.wetest.uia2.Main&quot;);
        // 定义正则表达式模式来匹配PID
        string pattern = @&quot;shell\s+(\d+)&quot;;
        // 创建Regex对象
        Regex regex = new(pattern);
        // 查找所有匹配的PID
        MatchCollection matches = regex.Matches(input);
        // 输出所有匹配的PID
        foreach (Match match in matches)
        {
            string pid = match.Groups[1].Value;
            NProcess.RunReturnString($&quot;adb -s {Serial} shell kill -9 {pid}&quot;);
            Log.Debug($&quot;kill uiautomatorServer pid: {pid}&quot;);
        }
    }

    public void Kill()
    {
        StopUiautomatorServer();
        if (_process != null &amp;#x26;&amp;#x26; !_process.HasExited)
        {
            try
            {
                _process.Kill();
                _process.WaitForExit();
                Log.Info(&quot;Process has been killed and exited.&quot;);
            }
            catch (Exception ex)
            {
                Log.Error($&quot;Failed to kill the process: {ex.Message}&quot;);
            }
        }
    }

    private void OnProcessExit(object sender, EventArgs e)
    {
        Kill();
    }

    public void Dispose()
    {
        Kill();
    }
}

public class U2Device
{
    private MockAdbProcess _process;
    private string _serial = string.Empty;
    private ADB _adb;
    private static readonly SemaphoreSlim _semaphore = new(1, 1);  // 创建一个 SemaphoreSlim，最大并发数为 1
    private string urlBase = &quot;http://127.0.0.1&quot;;

    public string Serial
    {
        get { return _serial; }
        set { _serial = value; }
    }

    /// &amp;#x3C;summary&gt;
    /// 构造方法。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;serial&quot;&gt;设备的序列号。如果未指定，则使用第一个连接的设备。&amp;#x3C;/param&gt;
    public U2Device(string serial = &quot;&quot;)
    {
        if (string.IsNullOrEmpty(serial))
        {
            List&amp;#x3C;string&gt; devices = ADB.Devices();
            if (devices.Count &gt; 0)
            {
                Serial = devices[0]; // 如果有设备连接，则使用第一个设备
            }
            else
            {
                throw new InvalidOperationException(&quot;no devices/emulators found&quot;);
            }
        }
        else
        {
            Serial = serial;
        }
        _adb = new ADB(Serial);
        string port = _adb.GetForwardPort();
        urlBase = $&quot;http://127.0.0.1:{port}&quot;;
    }

    /// &amp;#x3C;summary&gt;
    /// 安装U2工具的JAR文件。如果指定了重新安装，则先删除现有的JAR文件，然后检查并推送新的JAR文件到指定目录。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;reinstall&quot;&gt;是否重新安装U2工具的JAR文件，默认为false。&amp;#x3C;/param&gt;
    public void SetupJar(bool reinstall=false)
    {
        if (reinstall)
        {
            _adb.FileRemove(&quot;/data/local/tmp/u2.jar&quot;);
        }
        if (!_adb.FileExists(&quot;/data/local/tmp/u2.jar&quot;))
        {
            _adb.Push(&quot;Assets/u2/u2.jar&quot;, &quot;/data/local/tmp&quot;);
        }
    }

    /// &amp;#x3C;summary&gt;
    /// 检查U2设备是否在线（响应&quot;pong&quot;）。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;returns&gt;如果设备在线并响应&quot;pong&quot;，则返回true；否则返回false。&amp;#x3C;/returns&gt;
    public bool Live()
    {
        string r = Requests.Get($&quot;{urlBase}/ping&quot;).Result;
        return Equals(r, &quot;pong&quot;);
    }

    /// &amp;#x3C;summary&gt;
    /// 等待U2服务在线，直到超时。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;timeout&quot;&gt;等待的超时时间（秒），默认为30秒。&amp;#x3C;/param&gt;
    /// &amp;#x3C;returns&gt;如果在超时时间内U2服务在线，则返回true；否则返回false。&amp;#x3C;/returns&gt;
    public bool Wait(int timeout=30)
    {
        DateTime startTime = DateTime.Now;
        while (DateTime.Now - startTime &amp;#x3C; TimeSpan.FromSeconds(timeout))
        {
            if (Live())
                return true;
        }
        return false;
    }

    /// &amp;#x3C;summary&gt;
    /// 启动设备的U2服务，并等待服务在线。
    /// &amp;#x3C;/summary&gt;
    public void LaunchUiautomator()
    {
        _process = new(Serial);
        string port = _adb.GetForwardPort();
        urlBase = $&quot;http://127.0.0.1:{port}&quot;;
        Wait();
    }

    /// &amp;#x3C;summary&gt;
    /// 启动设备的U2服务。如果服务已经在运行，则直接返回；否则，安装U2并启动服务。
    /// &amp;#x3C;/summary&gt;
    public void StartUiautomator()
    {
        // 检查u2服务是否正在运行
        if (Live())
        {
            return;
        }
        // 安装u2
        if (!_adb.FileExists(&quot;/data/local/tmp/u2.jar&quot;))
        {
            SetupJar();
        }
        // 启动u2服务
        LaunchUiautomator();
    }

    /// &amp;#x3C;summary&gt;
    /// 停止设备的U2服务。
    /// &amp;#x3C;/summary&gt;
    public void StopUiautomator()
    {
        _process?.Kill();
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上执行Shell命令。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;cmd&quot;&gt;要在设备上执行的Shell命令。&amp;#x3C;/param&gt;
    /// &amp;#x3C;returns&gt;返回命令执行的结果字符串。&amp;#x3C;/returns&gt;
    public string Shell(string cmd)
    {
        return _adb.Shell(cmd);
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上模拟按键操作。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;keyCode&quot;&gt;要模拟的按键。&amp;#x3C;/param&gt;
    public void Press(KeyCode keyCode)
    {
        // 字典映射 KeyCode 到按键字符串
        var keyMapping = new Dictionary&amp;#x3C;KeyCode, string&gt;
        {
            { KeyCode.Home, &quot;home&quot; },
            { KeyCode.Back, &quot;back&quot; },
            { KeyCode.Left, &quot;left&quot; },
            { KeyCode.Right, &quot;right&quot; },
            { KeyCode.Up, &quot;up&quot; },
            { KeyCode.Down, &quot;down&quot; },
            { KeyCode.Center, &quot;center&quot; },
            { KeyCode.Menu, &quot;menu&quot; },
            { KeyCode.Search, &quot;search&quot; },
            { KeyCode.Enter, &quot;enter&quot; },
            { KeyCode.Delete, &quot;delete&quot; },
            { KeyCode.Recent, &quot;recent&quot; },
            { KeyCode.VolumeUp, &quot;volume_up&quot; },
            { KeyCode.VolumeDown, &quot;volume_down&quot; },
            { KeyCode.VolumeMute, &quot;volume_mute&quot; },
            { KeyCode.Camera, &quot;camera&quot; },
            { KeyCode.Power, &quot;power&quot; }
        };
        // 检查是否存在于映射中
        if (keyMapping.TryGetValue(keyCode, out string keyString))
        {
            JsonRpcCall(&quot;pressKey&quot;, [keyString]);
        }
        else
        {
            // 错误处理：当传入的 KeyCode 不在映射中时
            throw new ArgumentOutOfRangeException(nameof(keyCode), &quot;Invalid KeyCode value&quot;);
        }
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上模拟长按按键操作。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;keyCode&quot;&gt;要模拟长按的按键。&amp;#x3C;/param&gt;
    public void LongPress(KeyCode keyCode)
    {
        // 使用字典映射 KeyCode 到对应的 keyevent 数值
        var keyEventMapping = new Dictionary&amp;#x3C;KeyCode, int&gt;
        {
            { KeyCode.Home, 3 },
            { KeyCode.Back, 4 },
            { KeyCode.Left, 21 },
            { KeyCode.Right, 22 },
            { KeyCode.Up, 19 },
            { KeyCode.Down, 20 },
            { KeyCode.Center, 23 },
            { KeyCode.Menu, 82 },
            { KeyCode.Search, 84 },
            { KeyCode.Enter, 66 },
            { KeyCode.Delete, 67 },
            { KeyCode.Notification, 83 },
            { KeyCode.VolumeUp, 24 },
            { KeyCode.VolumeDown, 25 },
            { KeyCode.VolumeMute, 91 },
            { KeyCode.Power, 26 }
        };

        // 如果映射中不存在该按键，抛出异常
        if (!keyEventMapping.TryGetValue(keyCode, out int code))
        {
            throw new ArgumentOutOfRangeException(nameof(keyCode), &quot;Invalid KeyCode value&quot;);
        }

        // 执行 shell 命令
        Shell($&quot;input keyevent --longpress {code}&quot;);
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上模拟点击操作。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;x&quot;&gt;点击位置的X坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;y&quot;&gt;点击位置的Y坐标。&amp;#x3C;/param&gt;
    public void Click(double x, double y)
    {
        JsonRpcCall(&quot;click&quot;, [x, y]);
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上模拟长按操作。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;x&quot;&gt;长按位置的X坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;y&quot;&gt;长按位置的Y坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;duration&quot;&gt;长按的持续时间（秒），默认为0.5秒。&amp;#x3C;/param&gt;
    public void LongClick(double x, double y, double duration = 0.5)
    {
        JsonRpcCall(&quot;click&quot;, [x, y, duration*1000]);
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上模拟滑动操作。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;x1&quot;&gt;起始点的X坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;y1&quot;&gt;起始点的Y坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;x2&quot;&gt;终点X坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;y2&quot;&gt;终点Y坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;duration&quot;&gt;滑动操作的持续时间，默认为0。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;steps&quot;&gt;滑动操作的步数，默认为55。&amp;#x3C;/param&gt;
    public void Swipe(double x1, double y1, double x2, double y2, double duration=0, int steps=55)
    {
        if (duration != 0 &amp;#x26;&amp;#x26; steps != 0)
            duration = 0;  // duration与steps不能同时设置.
        if (duration != 0)
        {
            steps = (int)(duration * 200);
        }
        _adb.Rel2Abs(x1, y1, out double abs_x1, out double abs_y1);
        _adb.Rel2Abs(x2, y2, out double abs_x2, out double abs_y2);
        JsonRpcCall(&quot;swipe&quot;, [abs_x1, abs_y1, abs_x2, abs_y2, steps]);
    }

    /// &amp;#x3C;summary&gt;
    /// 在设备上模拟拖拽操作。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;x1&quot;&gt;起始点的X坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;y1&quot;&gt;起始点的Y坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;x2&quot;&gt;终点X坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;y2&quot;&gt;终点Y坐标。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;duration&quot;&gt;滑动操作的持续时间，默认为0.5。&amp;#x3C;/param&gt;
    public void Drag(double x1, double y1, double x2, double y2, double duration = 0.5)
    {
        _adb.Rel2Abs(x1, y1, out double abs_x1, out double abs_y1);
        _adb.Rel2Abs(x2, y2, out double abs_x2, out double abs_y2);
        JsonRpcCall(&quot;drag&quot;, [abs_x1, abs_y1, abs_x2, abs_y2, duration*200]);
    }

    /// &amp;#x3C;summary&gt; 唤醒屏幕 &amp;#x3C;/summary&gt;
    public void ScreenOn()
    {
        JsonRpcCall(&quot;wakeUp&quot;, []);
    }

    /// &amp;#x3C;summary&gt; 熄灭屏幕 &amp;#x3C;/summary&gt;
    public void ScreenOff()
    {
        JsonRpcCall(&quot;sleep&quot;, []);
    }

    /// &amp;#x3C;summary&gt;
    /// 获取屏幕旋转方向
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;return&gt;屏幕旋转方向的枚举值 &amp;#x3C;/return&gt;
    public Orientation GetOrientation()
    {
        // 获取设备信息
        string r = JsonRpcCall(&quot;deviceInfo&quot;, []);
        // 使用正则表达式提取 displayRotation 值
        string pattern = @&quot;&quot;&quot;displayRotation&quot;&quot;:(\d)&quot;;
        Regex regex = new(pattern);
        Match match = regex.Match(r);
        // 如果匹配成功，解析 orientation，否则返回默认值
        if (match.Success)
        {
            string orientation = match.Groups[1].Value;
            // 映射数字到对应的枚举值
            return orientation switch
            {
                &quot;0&quot; =&gt; Orientation.Natural,
                &quot;1&quot; =&gt; Orientation.Left,
                &quot;2&quot; =&gt; Orientation.Upsidedown,
                &quot;3&quot; =&gt; Orientation.Right,
                _ =&gt; Orientation.Natural, // 如果值不符合预期，则返回默认值
            };
        }
        // 如果没有匹配到，则返回默认值
        return Orientation.Natural;
    }

    /// &amp;#x3C;summary&gt;
    /// 设置屏幕旋转方向
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;orientation&quot;&gt;屏幕旋转方向的枚举值。&amp;#x3C;/param&gt;
    public void SetOrientation(Orientation orientation)
    {
        var keyMapping = new Dictionary&amp;#x3C;Orientation, string&gt;
        {
            { Orientation.Natural, &quot;natural&quot; },
            { Orientation.Left, &quot;left&quot; },
            { Orientation.Upsidedown, &quot;upsidedown&quot; },
            { Orientation.Right, &quot;right&quot; },
        };

        // 检查是否存在于映射中
        if (keyMapping.TryGetValue(orientation, out string keyString))
        {
            // 调用 JsonRpcCall
            JsonRpcCall(&quot;setOrientation&quot;, [keyString]);
        }
        else
        {
            // 错误处理：当传入的 KeyCode 不在映射中时
            throw new ArgumentOutOfRangeException(nameof(orientation), &quot;Invalid KeyCode value&quot;);
        }
    }

    /// &amp;#x3C;summary&gt; 打开通知栏 &amp;#x3C;/summary&gt;
    public void OpenNotification()
    {
        JsonRpcCall(&quot;openNotification&quot;, []);
    }

    /// &amp;#x3C;summary&gt; 打开快速设置&amp;#x3C;/summary&gt;
    public void OpenQuickSettings()
    {
        JsonRpcCall(&quot;openQuickSettings&quot;, []);
    }

    /// &amp;#x3C;summary&gt; 清空文本 &amp;#x3C;/summary&gt;
    public void ClearInputText()
    {
        JsonRpcCall(&quot;clearInputText&quot;, []);
        JsonRpcCall(&quot;clearInputText&quot;, []); // 执行两次  概率性无法清空
    }

    /// &amp;#x3C;summary&gt;
    /// 输入文本
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;text&quot;&gt;文本内容。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;clear&quot;&gt;是否清空原文本，默认false。&amp;#x3C;/param&gt;
    public void InputText(string text, bool clear=false)
    {
        if (clear)
        {
            ClearInputText();
            Sleep(500);
        }
        Shell($&quot;input text {text}&quot;);
    }

    /// &amp;#x3C;summary&gt;
    /// 获取属性值
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;prop&quot;&gt;需要获取的属性。&amp;#x3C;/param&gt;
    public string Getprop(string prop)
    {
        return Shell($&quot;getprop {prop}&quot;);
    }

    /// &amp;#x3C;summary&gt;
    /// 等待控件出现
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;selector&quot;&gt; 选择器 &amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;timeout&quot;&gt; 等待超时时间，默认20000ms &amp;#x3C;/param&gt;
    public void WaitForExists(Selector selector, int timeout= 20000)
    {
        JsonRpcCall(&quot;waitForExists&quot;, [selector.Contents, timeout]);
    }

    /// &amp;#x3C;summary&gt;
    /// 判断控件是否存在
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;selector&quot;&gt; 选择器 &amp;#x3C;/param&gt;
    /// &amp;#x3C;return&gt; 控件是否存在 &amp;#x3C;/return&gt;
    public bool Exists(Selector selector)
    {
        string r = JsonRpcCall(&quot;exist&quot;, [selector.Contents]);
        return r.Contains(&quot;true&quot;);
    }

    /// &amp;#x3C;summary&gt;
    /// 获取控件信息
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;selector&quot;&gt; 选择器 &amp;#x3C;/param&gt;
    /// &amp;#x3C;return&gt; 控件信息 &amp;#x3C;/return&gt;
    public Dictionary&amp;#x3C;string, object&gt; ObjInfo(Selector selector)
    {
        string r = JsonRpcCall(&quot;objInfo&quot;, [selector.Contents]);
        var jsonObject = JObject.Parse(r);
        // 提取 &quot;result&quot; 部分
        var result = jsonObject[&quot;result&quot;];
        // 将 &quot;result&quot; 部分转换为字典
        var resultDictionary = result.ToObject&amp;#x3C;Dictionary&amp;#x3C;string, object&gt;&gt;();
        // 处理 bounds，拆分成各个子字段并添加到字典中
        if (resultDictionary.ContainsKey(&quot;bounds&quot;))
        {
            // 拆分 &quot;bounds&quot; 中的字段并添加到字典
            if (resultDictionary[&quot;bounds&quot;] is JObject bounds)
            {
                resultDictionary[&quot;boundsBottom&quot;] = bounds[&quot;bottom&quot;];
                resultDictionary[&quot;boundsLeft&quot;] = bounds[&quot;left&quot;];
                resultDictionary[&quot;boundsRight&quot;] = bounds[&quot;right&quot;];
                resultDictionary[&quot;boundsTop&quot;] = bounds[&quot;top&quot;];
                resultDictionary.Remove(&quot;bounds&quot;);
            }
        }
        if (resultDictionary.ContainsKey(&quot;visibleBounds&quot;))
        {
            if (resultDictionary[&quot;visibleBounds&quot;] is JObject visibleBounds)
            {
                resultDictionary[&quot;visibleBoundsBottom&quot;] = visibleBounds[&quot;bottom&quot;];
                resultDictionary[&quot;visibleBoundsLeft&quot;] = visibleBounds[&quot;left&quot;];
                resultDictionary[&quot;visibleBoundsRight&quot;] = visibleBounds[&quot;right&quot;];
                resultDictionary[&quot;visibleBoundsTop&quot;] = visibleBounds[&quot;top&quot;];
                resultDictionary.Remove(&quot;visibleBounds&quot;);
            }
        }
        return resultDictionary;
    }

    /// &amp;#x3C;summary&gt;
    /// 点击控件
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;selector&quot;&gt; 选择器 &amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;delay&quot;&gt; 执行结束后等待时间，默认2秒 &amp;#x3C;/param&gt;
    public void ClickUi(Selector selector, int delay=2000)
    {
        WaitForExists(selector);
        Dictionary&amp;#x3C;string, object&gt; objInfo = ObjInfo(selector);
        double center_x = (Convert.ToInt32(objInfo[&quot;boundsLeft&quot;]) + Convert.ToInt32(objInfo[&quot;boundsRight&quot;])) / 2;
        double center_y = (Convert.ToInt32(objInfo[&quot;boundsTop&quot;]) + Convert.ToInt32(objInfo[&quot;boundsBottom&quot;])) / 2;
        Click(center_x, center_y);
        Sleep(delay);
    }

    /// &amp;#x3C;summary&gt;
    /// 查找控件
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;selector&quot;&gt; 选择器 &amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;isClick&quot;&gt; 是否点击 &amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;delay&quot;&gt; 执行结束后等待时间，默认2秒 &amp;#x3C;/param&gt;
    /// &amp;#x3C;returns&gt; 是否存在 &amp;#x3C;/returns&gt;
    public bool FindUi(Selector selector, bool isClick = false, int delay = 2000)
    {
        bool isExist = Exists(selector);
        if (isExist &amp;#x26;&amp;#x26; isClick)
        {
            ClickUi(selector, delay);
        }
        return isExist;
    }

    public static void Sleep(int sleepTime)
    {
        Thread.Sleep(sleepTime);
    }

    public static Selector By(string text = &quot;&quot;, string textContains = &quot;&quot;, string textMatches = &quot;&quot;, string textStartsWith = &quot;&quot;, string className = &quot;&quot;,
        string classNameMatches = &quot;&quot;, string description = &quot;&quot;, string descriptionContains = &quot;&quot;, string descriptionMatches = &quot;&quot;, string descriptionStartsWith = &quot;&quot;,
        string checkable = &quot;&quot;, string Checked = &quot;&quot;, string clickable = &quot;&quot;, string longClickable = &quot;&quot;, string scrollable = &quot;&quot;, string enabled = &quot;&quot;, string focusable = &quot;&quot;,
        string focused = &quot;&quot;, string selected = &quot;&quot;, string packageName = &quot;&quot;, string packageNameMatches = &quot;&quot;, string resourceId = &quot;&quot;, string resourceIdMatches = &quot;&quot;,
        string index = &quot;&quot;)
    {
        Selector selector = new();
        int mask = 0;
        if (!string.IsNullOrEmpty(text))
        {
            selector.Contents.Add(&quot;text&quot;, text);
            mask |= 1 &amp;#x3C;&amp;#x3C; 0;  // 0x01
        }
        if (!string.IsNullOrEmpty(textContains))
        {
            selector.Contents.Add(&quot;textContains&quot;, textContains);
            mask |= 1 &amp;#x3C;&amp;#x3C; 1;  // 0x02
        }
        if (!string.IsNullOrEmpty(textMatches))
        {
            selector.Contents.Add(&quot;textMatches&quot;, textMatches);
            mask |= 1 &amp;#x3C;&amp;#x3C; 2;  // 0x04
        }
        if (!string.IsNullOrEmpty(textStartsWith))
        {
            selector.Contents.Add(&quot;textStartsWith&quot;, textStartsWith);
            mask |= 1 &amp;#x3C;&amp;#x3C; 3;  // 0x08
        }
        if (!string.IsNullOrEmpty(className))
        {
            selector.Contents.Add(&quot;className&quot;, className);
            mask |= 1 &amp;#x3C;&amp;#x3C; 4;  // 0x10
        }
        if (!string.IsNullOrEmpty(classNameMatches))
        {
            selector.Contents.Add(&quot;classNameMatches&quot;, classNameMatches);
            mask |= 1 &amp;#x3C;&amp;#x3C; 5;  // 0x20
        }
        if (!string.IsNullOrEmpty(description))
        {
            selector.Contents.Add(&quot;description&quot;, description);
            mask |= 1 &amp;#x3C;&amp;#x3C; 6;  // 0x40
        }
        if (!string.IsNullOrEmpty(descriptionContains))
        {
            selector.Contents.Add(&quot;descriptionContains&quot;, descriptionContains);
            mask |= 1 &amp;#x3C;&amp;#x3C; 7;  // 0x80
        }
        if (!string.IsNullOrEmpty(descriptionMatches))
        {
            selector.Contents.Add(&quot;descriptionMatches&quot;, descriptionMatches);
            mask |= 1 &amp;#x3C;&amp;#x3C; 8;  // 0x0100
        }
        if (!string.IsNullOrEmpty(descriptionStartsWith))
        {
            selector.Contents.Add(&quot;descriptionStartsWith&quot;, descriptionStartsWith);
            mask |= 1 &amp;#x3C;&amp;#x3C; 9;  // 0x0200
        }
        if (!string.IsNullOrEmpty(checkable))
        {
            selector.Contents.Add(&quot;checkable&quot;, checkable);
            mask |= 1 &amp;#x3C;&amp;#x3C; 10;  // 0x0400
        }
        if (!string.IsNullOrEmpty(Checked))
        {
            selector.Contents.Add(&quot;checked&quot;, Checked);
            mask |= 1 &amp;#x3C;&amp;#x3C; 11;  // 0x0800
        }
        if (!string.IsNullOrEmpty(clickable))
        {
            selector.Contents.Add(&quot;clickable&quot;, clickable);
            mask |= 1 &amp;#x3C;&amp;#x3C; 12;  // 0x1000
        }
        if (!string.IsNullOrEmpty(longClickable))
        {
            selector.Contents.Add(&quot;longClickable&quot;, longClickable);
            mask |= 1 &amp;#x3C;&amp;#x3C; 13;  // 0x2000
        }
        if (!string.IsNullOrEmpty(scrollable))
        {
            selector.Contents.Add(&quot;scrollable&quot;, scrollable);
            mask |= 1 &amp;#x3C;&amp;#x3C; 14;  // 0x4000
        }
        if (!string.IsNullOrEmpty(enabled))
        {
            selector.Contents.Add(&quot;enabled&quot;, enabled);
            mask |= 1 &amp;#x3C;&amp;#x3C; 15;  // 0x8000
        }
        if (!string.IsNullOrEmpty(focusable))
        {
            selector.Contents.Add(&quot;focusable&quot;, focusable);
            mask |= 1 &amp;#x3C;&amp;#x3C; 16;  // 0x010000
        }
        if (!string.IsNullOrEmpty(focused))
        {
            selector.Contents.Add(&quot;focused&quot;, focused);
            mask |= 1 &amp;#x3C;&amp;#x3C; 17;  // 0x020000
        }

        if (!string.IsNullOrEmpty(selected))
        {
            selector.Contents.Add(&quot;selected&quot;, selected);
            mask |= 1 &amp;#x3C;&amp;#x3C; 18;  // 0x040000
        }
        if (!string.IsNullOrEmpty(packageName))
        {
            selector.Contents.Add(&quot;packageName&quot;, packageName);
            mask |= 1 &amp;#x3C;&amp;#x3C; 19;  // 0x080000
        }
        if (!string.IsNullOrEmpty(packageNameMatches))
        {
            selector.Contents.Add(&quot;packageNameMatches&quot;, packageNameMatches);
            mask |= 1 &amp;#x3C;&amp;#x3C; 20;  // 0x100000
        }
        if (!string.IsNullOrEmpty(resourceId))
        {
            selector.Contents.Add(&quot;resourceId&quot;, resourceId);
            mask |= 1 &amp;#x3C;&amp;#x3C; 21;  // 0x200000
        }
        if (!string.IsNullOrEmpty(resourceIdMatches))
        {
            selector.Contents.Add(&quot;resourceIdMatches&quot;, resourceIdMatches);
            mask |= 1 &amp;#x3C;&amp;#x3C; 22;  // 0x400000
        }
        if (!string.IsNullOrEmpty(index))
        {
            selector.Contents.Add(&quot;index&quot;, index);
            mask |= 1 &amp;#x3C;&amp;#x3C; 23;  // 0x800000
        }
        selector.Contents.Add(&quot;mask&quot;, mask);
        selector.Mask = mask;

        return selector;
    }

    public string JsonRpcCall(string method, object[] param, int timeout = 60)
    {
        // 等待获取信号量，保证只有一个线程可以进入下面的异步调用
        _semaphore.WaitAsync();
        string r = String.Empty;
        try
        {
            if (!Live())
            {
                Log.Debug($&quot;uiautomator2 no ok&quot;);
                StopUiautomator();
                StartUiautomator();
            }
            r = _JsonRpcCall(method, param, timeout);
            //Log.Info(r);

            if (r.Contains(&quot;An error occurred while sending the request&quot;) || r.Contains(&quot;无法连接&quot;))
            {
                Log.Debug($&quot;uiautomator2 no ok&quot;);
                StopUiautomator();
                StartUiautomator();
                _JsonRpcCall(method, param, timeout);
            }
            else if (r.Contains(&quot;UiObjectNotFound&quot;))
            {
                Log.Error(r);
                throw new UiObjectNotFoundError(r);
            } else if (r.Contains(&quot;IllegalStateException&quot;))
            {
                Log.Error(r);
                throw new IllegalStateException(r);
            }
        }
        catch (Exception ex)
        {
            Log.Error($&quot;JsonRpcCall error: {ex.Message}&quot;);
            throw;
        }
        finally
        {
            // 确保释放信号量，允许其他线程继续执行
            _semaphore.Release();
        }
        return r;
    }

    public string _JsonRpcCall(string method, object[] param, int timeout = 60)
    {
        // 使用字典创建 JSON-RPC 2.0 请求数据
        var content = new Dictionary&amp;#x3C;string, object&gt;
        {
            { &quot;jsonrpc&quot;, &quot;2.0&quot; },
            { &quot;id&quot;, 1 },
            { &quot;method&quot;, method },
            { &quot;params&quot;, param }  // params 使用对象数组
        };
        //Log.Debug($&quot;method: {method}, params: {FormatParameters(param)}, timeout: {timeout}&quot;);
        return Requests.Post($&quot;{urlBase}/jsonrpc/0&quot;, JsonConvert.SerializeObject(content), timeout);
    }

    private string FormatParameters(object[] param)
    {
        if (param == null || param.Length == 0)
        {
            return &quot;empty&quot;;
        }
        return string.Join(&quot;, &quot;, param.Select(p =&gt; FormatObject(p)));
    }

    private string FormatObject(object obj)
    {
        if (obj == null)
        {
            return &quot;null&quot;;
        }
        // 如果是字典类型，递归格式化字典内容
        if (obj is Dictionary&amp;#x3C;string, object&gt; dict)
        {
            return &quot;{&quot; + string.Join(&quot;, &quot;, dict.Select(kv =&gt; $&quot;{kv.Key}: {FormatObject(kv.Value)}&quot;)) + &quot;}&quot;;
        }
        // 如果是列表类型（例如 List&amp;#x3C;object&gt;），递归格式化列表元素
        if (obj is IEnumerable&amp;#x3C;object&gt; enumerable)
        {
            return &quot;[&quot; + string.Join(&quot;, &quot;, enumerable.Select(e =&gt; FormatObject(e))) + &quot;]&quot;;
        }
        // 如果是DateTime类型，返回自定义格式的日期
        if (obj is DateTime)
        {
            return ((DateTime)obj).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        }
        // 默认返回对象的 ToString() 值
        return obj.ToString();
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.BcxBLtkC.jpg"/><enclosure url="/_astro/thumbnail.BcxBLtkC.jpg"/></item><item><title>截取指定区域图像</title><link>https://saneko.me/blog/613c6215985f</link><guid isPermaLink="true">https://saneko.me/blog/613c6215985f</guid><description>介绍如何实现区域截图功能，包含基本方法和应用场景。</description><pubDate>Tue, 28 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;开发背景&lt;/h3&gt;
&lt;p&gt;一般来说，我们可以通过&lt;code&gt;adb shell screencap&lt;/code&gt;指令来截取android设备中的图像，但是获取到的是完整的图像。当我们需要进行图像识别的操作时，需要获取指定区域的图像来进行对比。&lt;/p&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;
&lt;h4&gt;C#实现&lt;/h4&gt;
&lt;p&gt;部分代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;/// &amp;#x3C;summary&gt;
/// 对指定设备进行屏幕截图并裁剪指定区域，然后将裁剪后的图像保存到指定路径。
/// &amp;#x3C;/summary&gt;
/// &amp;#x3C;param name=&quot;x&quot;&gt;裁剪区域左上角的x坐标。&amp;#x3C;/param&gt;
/// &amp;#x3C;param name=&quot;y&quot;&gt;裁剪区域左上角的y坐标。&amp;#x3C;/param&gt;
/// &amp;#x3C;param name=&quot;width&quot;&gt;裁剪区域的宽度。&amp;#x3C;/param&gt;
/// &amp;#x3C;param name=&quot;height&quot;&gt;裁剪区域的高度。&amp;#x3C;/param&gt;
/// &amp;#x3C;param name=&quot;savePath&quot;&gt;裁剪后图像的保存路径，默认值为&quot;screencap.png&quot;。&amp;#x3C;/param&gt;
public void ScreenCapAndCrop(int x, int y, int width, int height, string savePath = &quot;screencap.png&quot;)
{
    Stream stream = NProcess.RunReturnStream($&quot;adb -s {_serial} shell screencap -p&quot;);
    List&amp;#x3C;byte&gt; data = ReadStreamAndConvertCRLF(stream);
    if (data.Count == 0)
    {
        Log.Error($&quot;{_serial} screencap failed!&quot;);
        return;
    }
    // 将字节列表转换为字节数组
    byte[] imageData = [.. data];

    // 使用字节数组创建一个图像对象
    using MemoryStream ms = new(imageData);
    using Bitmap bitmap = new(ms);
    // 创建裁剪区域
    Rectangle cropRect = new(x, y, width, height);

    // 裁剪图像
    using Bitmap croppedImage = bitmap.Clone(cropRect, bitmap.PixelFormat);
    // 保存裁剪后的图像
    croppedImage.Save(savePath, ImageFormat.Png);
}

/// &amp;#x3C;summary&gt;
/// 从给定的流中读取数据，并处理回车符（CR）和换行符（LF），将CRLF转换为LF
/// &amp;#x3C;/summary&gt;
/// &amp;#x3C;param name=&quot;stream&quot;&gt;要从中读取数据的流&amp;#x3C;/param&gt;
/// &amp;#x3C;returns&gt;处理后的字节列表，其中CRLF被转换为LF&amp;#x3C;/returns&gt;
private static List&amp;#x3C;byte&gt; ReadStreamAndConvertCRLF(Stream stream)
{
    ArgumentNullException.ThrowIfNull(stream);

    List&amp;#x3C;byte&gt; data = [];
    byte[] buffer = new byte[1024];
    int read;
    bool isCR = false;
    do
    {
        byte[] buf = new byte[1024];
        read = stream.Read(buf, 0, buf.Length);

        for (int i = 0; i &amp;#x3C; read; i++) //convert CRLF to LF
        {
            if (isCR &amp;#x26;&amp;#x26; buf[i] == 0x0A)
            {
                isCR = false;
                data.RemoveAt(data.Count - 1);
                data.Add(buf[i]);
                continue;
            }
            isCR = buf[i] == 0x0D;
            data.Add(buf[i]);
        }
    }
    while (read &gt; 0);
    return data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_serial&lt;/code&gt;为设备号，通过adb devices获取，具体代码就不列举了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;python实现&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import subprocess
from PIL import Image
import io

def get_screenshot_and_crop(x, y, width, height, output_path=&quot;screencap.png&quot;):
    process = subprocess.Popen(
        [&apos;adb&apos;, &apos;shell&apos;, &apos;screencap&apos;, &apos;-p&apos;],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout, stderr = process.communicate()

    # 处理 Windows 系统中的换行符问题
    if b&apos;\r\n&apos; in stdout:
        stdout = stdout.replace(b&apos;\r\n&apos;, b&apos;\n&apos;)

    # 将二进制数据转换为图像并裁剪
    image = Image.open(io.BytesIO(stdout))
    cropped_image = image.crop((x, y, x + width, y + height))
    # 保存裁剪后的图像
    cropped_image.save(output_path)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.BULWS5DF.jpg"/><enclosure url="/_astro/thumbnail.BULWS5DF.jpg"/></item><item><title>博客封面图制作</title><link>https://saneko.me/blog/b232bd1242b8</link><guid isPermaLink="true">https://saneko.me/blog/b232bd1242b8</guid><description>本文介绍博客封面图的制作流程。</description><pubDate>Thu, 23 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;前提&lt;/h3&gt;
&lt;p&gt;之前我的博客页面都是直接用AI生成的，风格各异，有些甚至看起来很奇怪，整体效果也不是很理想。后来，我看到了&lt;a href=&quot;https://blog.zhheo.com/p/463d306b.html&quot;&gt;张洪Heo&lt;/a&gt;的博客封面图，非常吸引人，颜色搭配和设计都很有美感。但是，他的方法对我来说操作起来有一定的难度，不太容易上手。&lt;/p&gt;
&lt;p&gt;于是，我在知乎上搜索了&lt;a href=&quot;https://zhuanlan.zhihu.com/p/677112841&quot;&gt;如何制作文章封面图&lt;/a&gt;，找到了一些实用的方法。根据我自己的实际情况，对这些方法进行了适当的调整，尝试制作封面图。虽然我已经尽力去改进了，但效果还是比不上张洪Heo的作品那么完美。&lt;/p&gt;
&lt;h3&gt;制作工具&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MasterGo&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;https://mastergo.com/&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;boardmix&lt;/strong&gt;（之前用来画思维导图，开了1年会员，发现用来做图也很方便）
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://boardmix.cn/&quot;&gt;boardmix博思白板官网, AIGC在线生成, 多人协同思维导图, 流程图工具&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图标资源&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://igoutu.cn/icon/set/%E6%A0%87%E5%BF%97/fluency&quot;&gt;Windows 11 Color风格的标志符号和图标，格式有PNG、SVG&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;制作图标(可选)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;boardmix中，使用圆角矩形画出图标背景。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;不展示边框&lt;/li&gt;
&lt;li&gt;宽度256&lt;/li&gt;
&lt;li&gt;高度256&lt;/li&gt;
&lt;li&gt;圆角45&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250123_093548.png&quot; alt=&quot;图标背景&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;添加对应的图标素材，大小调整为210-240左右&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250123_131427.png&quot; alt=&quot;添加素材&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;选中区域，右键导出为png。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;制作封面&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;MasterGo中新建文件，容器选择社媒，公众号封面 2.35:1(900 x 383), 设置图标近似的背景色&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250123_131944.png&quot; alt=&quot;封面背景&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;添加文字，文字中间通过空格空出合适的位置&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;阿里妈妈数黑体&lt;/li&gt;
&lt;li&gt;字体大小130&lt;/li&gt;
&lt;li&gt;居中对齐&lt;/li&gt;
&lt;li&gt;自动高度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250123_132616.png&quot; alt=&quot;添加文字&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;添加刚才制作的图标，调整合适的大小，并添加阴影&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250123_132806.png&quot; alt=&quot;添加阴影&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;导出为png即可&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/thumbnail.1TJxb6ZI.jpg"/><enclosure url="/_astro/thumbnail.1TJxb6ZI.jpg"/></item><item><title>anzhiyu主题about页面鼠标动效不生效问题</title><link>https://saneko.me/blog/e3768cb83c14</link><guid isPermaLink="true">https://saneko.me/blog/e3768cb83c14</guid><description>本文介绍anzhiyu主题about页面鼠标动效失效的原因及解决方法。</description><pubDate>Thu, 16 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;问题背景&lt;/h3&gt;
&lt;p&gt;在配置&lt;a href=&quot;https://github.com/anzhiyu-c/hexo-theme-anzhiyu&quot;&gt;安知鱼主题&lt;/a&gt;的关于页面时，发现&lt;strong&gt;helloAbout&lt;/strong&gt;的动效突然不起作用了。如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_170327.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;原因分析&lt;/h3&gt;
&lt;p&gt;打开调试控制台，发现在about页面存在报错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_170745.png&quot; alt=&quot;控制台信息&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过排查发现，在&lt;code&gt;selfInfo&lt;/code&gt;中的&lt;code&gt;selfInfoContentYear&lt;/code&gt;我填的不是纯数字导致的。&lt;/p&gt;
&lt;h4&gt;修改前&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;selfInfo:
  selfInfoTips1: 生日
  selfInfoContentYear: 3 月 5 日
  selfInfoTips2: 星座
  selfInfoContent2: 双鱼座
  selfInfoTips3: 职业
  selfInfoContent3: 自动化工程师
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;修改后&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;selfInfo:
  selfInfoTips1: 出生
  selfInfoContentYear: 1999
  selfInfoTips2: 星座
  selfInfoContent2: 双鱼座
  selfInfoTips3: 职业
  selfInfoContent3: 自动化工程师
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;恢复正常&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_173812.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Dg5l_Wgh.jpg"/><enclosure url="/_astro/thumbnail.Dg5l_Wgh.jpg"/></item><item><title>gitignore添加文件不起作用</title><link>https://saneko.me/blog/9021777211af</link><guid isPermaLink="true">https://saneko.me/blog/9021777211af</guid><description>本文介绍.gitignore文件添加后不生效的原因及解决方法。</description><pubDate>Wed, 15 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;问题描述&lt;/h3&gt;
&lt;p&gt;通过git进行版本管理时，可以通过&lt;code&gt;.gitignore&lt;/code&gt;文件来配置哪些文件或目录应该被忽略，不纳入版本控制系统的管理范围。但是有时在&lt;code&gt;.gitignore&lt;/code&gt;文件中添加对应目录或文件后，在Sourcetree的未暂存文件中仍能看到。&lt;/p&gt;
&lt;h3&gt;可能原因&lt;/h3&gt;
&lt;p&gt;可能在之前已经将该目录或文件添加到了 Git 的暂存区或已经进行了版本控制，所以 &lt;code&gt;.gitignore&lt;/code&gt; 的规则对这些文件暂时不起作用。&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;p&gt;通过以下命令清除已经添加到暂存区的目录或文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;git rm -r --cached EEITest/app/build
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.mBl8FwGI.jpg"/><enclosure url="/_astro/thumbnail.mBl8FwGI.jpg"/></item><item><title>配置用户环境变量</title><link>https://saneko.me/blog/e459d496f60c</link><guid isPermaLink="true">https://saneko.me/blog/e459d496f60c</guid><description>本文介绍如何在Windows系统下自动添加用户环境变量PATH，支持C#和Python实现，避免重复添加并检查管理员权限，适用于自动化部署和软件安装场景。</description><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;
import { Tabs, TabItem } from &apos;astro-pure/user&apos;;&lt;/p&gt;
&lt;h3&gt;开发背景&lt;/h3&gt;
&lt;p&gt;在 Windows 系统应用开发与部署中，环境变量 &lt;code&gt;PATH&lt;/code&gt; 对查找可执行文件和库文件很重要。手动添加新路径到 &lt;code&gt;PATH&lt;/code&gt; 繁琐易错，在多用户环境下还会影响系统稳定性。&lt;/p&gt;
&lt;p&gt;因此，开发了一个自动化函数用于向用户级 &lt;code&gt;PATH&lt;/code&gt; 添加新路径。因修改需管理员权限，该函数会检查权限，同时避免重复添加，以提高效率和系统可维护性、可扩展性，方便软件安装更新等操作。&lt;/p&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;</content:encoded><h:img src="/_astro/thumbnail.CwKf9Tz1.jpg"/><enclosure url="/_astro/thumbnail.CwKf9Tz1.jpg"/></item><item><title>基于QThread的多线程任务管理</title><link>https://saneko.me/blog/84544b8bdbe9</link><guid isPermaLink="true">https://saneko.me/blog/84544b8bdbe9</guid><description>介绍如何用Python实现多线程任务管理，包含基本原理和代码示例。</description><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在开发一个 &lt;strong&gt;PyQt&lt;/strong&gt; 应用程序时，我们经常需要执行一些耗时的操作，例如&lt;strong&gt;文件读写&lt;/strong&gt;、&lt;strong&gt;网络请求&lt;/strong&gt;、&lt;strong&gt;复杂的数据处理&lt;/strong&gt;或&lt;strong&gt;长时间的计算&lt;/strong&gt;。如果这些操作在主线程中执行，会导致用户界面（UI）冻结，用户无法与应用程序进行交互，影响用户体验。为了解决这个问题，我们需要将这些耗时操作放在单独的线程中执行。&lt;/p&gt;
&lt;p&gt;然而，直接使用多线程会带来一些复杂性，例如线程的&lt;strong&gt;创建&lt;/strong&gt;、&lt;strong&gt;管理&lt;/strong&gt;、&lt;strong&gt;信号&lt;/strong&gt;和&lt;strong&gt;槽机制&lt;/strong&gt;的使用，以及在多线程环境下处理异常和结果的传递等。为了简化这些操作，因此开发了 &lt;strong&gt;Task&lt;/strong&gt; 和 &lt;strong&gt;TaskManager&lt;/strong&gt; 类。&lt;/p&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from PyQt5.QtCore import QThread, pyqtSignal

class Task(QThread):
    &quot;&quot;&quot;
    Task class to run a task in a separate thread.
    &quot;&quot;&quot;
    success_signal = pyqtSignal(object)
    fail_signal = pyqtSignal(Exception)
    finished_signal = pyqtSignal()

    def __init__(self, func, *args):
        super().__init__()
        self.func = func
        self.args = args

    def run(self):
        try:
            r = self.func(*self.args)
            self.success_signal.emit(r)
        except Exception as e:
            self.fail_signal.emit(e)
        self.finished_signal.emit()

    def onSuccess(self, func):
        self.success_signal.connect(func)
        return self

    def onFail(self, func):
        self.fail_signal.connect(func)
        return self

    def onFinished(self, func):
        self.finished_signal.connect(func)
        return self

class TaskManager:
    &quot;&quot;&quot;
    TaskManager class to manage multiple tasks.
    &quot;&quot;&quot;
    _instance = None
    _tasks = {}

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    @classmethod
    def create_task(cls, func, *args, **kwargs):
        if &quot;label&quot; in kwargs:
            label = kwargs[&quot;label&quot;]
        else:
            label = &quot;null&quot;
        task = Task(func, *args)
        if label not in cls._tasks:
            cls._tasks[label] = [task]
        else:
            cls._tasks[label].append(task)
        return task

    @classmethod
    def run_task(cls, task: Task):
        task.start()
        task.onFinished(lambda: cls.remove_task(task))
        return task

    @classmethod
    def run_tasks(cls, tasks: list):
        for task in tasks:
            if isinstance(task, Task):
                cls.run_task(task)

    @classmethod
    def remove_task(cls, task: Task):
        for key, value in cls._tasks.items():
            if task in value:
                value.remove(task)
                if len(value) == 0:
                    cls._tasks.pop(key)
                break

    @classmethod
    def is_idle(cls, label: str = &quot;null&quot;):
        return label not in cls._tasks

    @classmethod
    def is_all_idle(cls):
        return len(cls._tasks) == 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法解释&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程执行&lt;/strong&gt;：&lt;code&gt;Task&lt;/code&gt; 类继承自 &lt;code&gt;QThread&lt;/code&gt;，用于将函数封装在独立线程中执行。通过 &lt;code&gt;start()&lt;/code&gt; 方法在后台运行耗时操作，避免阻塞主线程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;信号机制&lt;/strong&gt;：使用 PyQt 的 &lt;code&gt;pyqtSignal&lt;/code&gt; 机制，&lt;code&gt;Task&lt;/code&gt; 类定义了三个信号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;success_signal：任务成功完成时触发&lt;/li&gt;
&lt;li&gt;fail_signal：任务执行异常时触发&lt;/li&gt;
&lt;li&gt;finished_signal：任务完成时触发（无论成功或失败）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;回调函数连接&lt;/strong&gt;：通过 &lt;code&gt;onSuccess&lt;/code&gt;、&lt;code&gt;onFail&lt;/code&gt; 和 &lt;code&gt;onFinished&lt;/code&gt; 方法连接回调函数，实现任务成功时更新 UI、失败时显示错误、完成时执行清理等操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;任务生命周期管理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create_task：创建任务并添加到 &lt;code&gt;_tasks&lt;/code&gt; 字典&lt;/li&gt;
&lt;li&gt;run_task：启动单个任务，完成后自动移除&lt;/li&gt;
&lt;li&gt;run_tasks：同时运行多个任务&lt;/li&gt;
&lt;li&gt;remove_task：移除已完成的任务，避免内存泄漏&lt;/li&gt;
&lt;li&gt;is_idle / is_all_idle：检查任务是否处于空闲状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用示例&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def getDeviceInfo():
    # 一些耗时操作...
    return &quot;模拟设备数据&quot;

task = TaskManager.create_task(getDeviceInfo).onSuccess(lambda deviceInfo: print(deviceInfo)).onFail(lambda e: print(e)).onFinished(lambda: print(&quot;任务结束&quot;))
TaskManager.run_task(task)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.1GNFLmUD.jpg"/><enclosure url="/_astro/thumbnail.1GNFLmUD.jpg"/></item><item><title>链式调用shell指令</title><link>https://saneko.me/blog/cd6d5d23606a</link><guid isPermaLink="true">https://saneko.me/blog/cd6d5d23606a</guid><description>介绍如何用Python实现Shell命令的链式调用，包含基本方法和应用场景。</description><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;开发背景&lt;/h3&gt;
&lt;p&gt;在自动化测试和脚本化任务中，常需按步骤执行 &lt;code&gt;adb shell&lt;/code&gt; 命令及处理结果。例如自动化测试时，先通过 &lt;code&gt;shell&lt;/code&gt; 命令安装应用，再检查安装结果，之后进行其他操作。&lt;/p&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class ADBShell:
    def __init__(self, device: str):
        self.shell = None  # shell 进程对象
        self.device = device
        self.stdout_adb = &quot;&quot;
        self.stderr_adb = &quot;&quot;
        self.start_shell()

    def start_shell(self):
        self.stdout_adb = &quot;&quot;
        self.stderr_adb = &quot;&quot;
        self.shell = subprocess.Popen([&quot;adb&quot;, &quot;-s&quot;, self.device, &quot;shell&quot;], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    def write(self, command: str):
        if self.shell.stdin.closed:  # 如果输入流已关闭，则重新启动 ADB shell 进程
            self.start_shell()
        if command[-1]!= &apos;\n&apos;:
            command += &apos;\n&apos;
        self.shell.stdin.write(command.encode())
        self.shell.stdin.flush()
        return self

    def read(self):
        if self.shell.stdin:
            self.shell.stdin.flush()
            self.shell.stdin.close()
        stdout, stderr = self.shell.communicate()
        return stdout.decode(), stderr.decode()

    def write_adb(self, adb_command: str):
        full_command = [&quot;adb&quot;, &quot;-s&quot;, self.device] + adb_command.split()
        process = subprocess.Popen(full_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = process.communicate()
        self.stdout_adb += stdout.decode()
        self.stderr_adb += stderr.decode()
        return self

    def read_adb(self):
        return self.stdout_adb, self.stderr_adb

    def flush(self):
        self.shell.stdin.flush()
        return self

    def close(self):
        if self.shell.stdin:
            self.shell.stdin.close()
        return self

    def sleep(self, sleep_time):
        time.sleep(sleep_time)
        return self
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用示例&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;通过adb将APK安装到手机中，并获取手机中APK的信息。获取完成后，将数据导出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;adbShell = ADBShell(&quot;T442A02XNKH0189&quot;)
_ = (adbShell.   # 链式执行删除结果，安装APK，启动APK等操作
        write(&quot;cd /sdcard/Android/data/com.tt.appinfo/files/&quot;).
        write(&quot;rm result.txt&quot;).
        write_adb(&quot;install app-debug.apk&quot;).
        write(&quot;am start -n com.tt.appinfo/.MainActivity&quot;).
        read())
start = time.time()
while((time.time() - start) &amp;#x3C; 30):    # 等待执行结束
    stdout, stderr = adbShell.write(f&quot;cd /sdcard/Android/data/com.tt.appinfo/files/&quot;).write(&quot;cat result.txt&quot;).read()
    if &quot;1&quot; in stdout:
        break
    time.sleep(1)
else:
    print(&quot;timeout&quot;)
adbShell.write_adb(&quot;pull /sdcard/Android/data/com.tt.appinfo/files/appInfo.db&quot;)   # 导出结果
print(&quot;finished&quot;)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.Voz-WJPg.jpg"/><enclosure url="/_astro/thumbnail.Voz-WJPg.jpg"/></item><item><title>Android配置uiautomator</title><link>https://saneko.me/blog/cabc72eceae5</link><guid isPermaLink="true">https://saneko.me/blog/cabc72eceae5</guid><description>本文介绍在 Android 项目中配置 UI Automator：添加依赖、在 androidTest 中编写用例、安装 APK 并通过 adb 运行测试。</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;p&gt;在build.gradle中添加&lt;strong&gt;uiautomator&lt;/strong&gt;依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;androidTestImplementation &apos;androidx.test.uiautomator:uiautomator:2.2.0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_231853.png&quot; alt=&quot;添加依赖&quot;&gt;&lt;/p&gt;
&lt;h3&gt;编写用例&lt;/h3&gt;
&lt;p&gt;需要在&lt;strong&gt;androidTest&lt;/strong&gt;中编写用例，而不是在main中。新建一个&lt;code&gt;test&lt;/code&gt;类，下面是一个打开设置的一个测试。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.tt.demo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class test {
    static UiDevice device;
    @Test
    public void openSettings() throws InterruptedException {
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        UiObject2 settings = device.findObject(By.text(&quot;Settings&quot;));
        if(settings!=null){
            settings.click();
            Thread.sleep(2000);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装APK&lt;/h3&gt;
&lt;p&gt;需要安装2个apk，一个是由main中编译出来的apk(傀儡应用)，另一个则是由androidTest编译出来的apk。可以直接通过右侧的Gradle中的install直接安装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232043.png&quot; alt=&quot;安装APK&quot;&gt;&lt;/p&gt;
&lt;h3&gt;运行测试&lt;/h3&gt;
&lt;p&gt;打开命令提示符，输入以下指令即可开始进行测试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb shell am instrument -w -e class com.tt.demo.test#openSettings com.tt.demo.test/androidx.test.runner.AndroidJUnitRunner
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.com/tools/adb?hl=zh-cn#am&quot;&gt;instrument参数&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Bk1k74fQ.jpg"/><enclosure url="/_astro/thumbnail.Bk1k74fQ.jpg"/></item><item><title>填充联系人、通话记录、短信的方法</title><link>https://saneko.me/blog/075493e7a78b</link><guid isPermaLink="true">https://saneko.me/blog/075493e7a78b</guid><description>介绍如何在Android中添加联系人、通话记录和短信，包含实现方法和注意事项。</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Tabs, TabItem } from &apos;astro-pure/user&apos;;&lt;/p&gt;
&lt;h2&gt;前提&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;Android自动化测试&lt;/code&gt;中，通过自动化测试脚本填充&lt;code&gt;联系人&lt;/code&gt;、&lt;code&gt;通话记录&lt;/code&gt;、&lt;code&gt;短信&lt;/code&gt;等数据，可以模拟出更接近真实用户的使用场景。这种模拟有助于发现那些在日常使用中才会出现的问题，从而提高测试的全面性和准确性。&lt;/p&gt;
&lt;h2&gt;实现方法&lt;/h2&gt;
&lt;h3&gt;联系人&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;uses-permission android:name=&quot;android.permission.READ_CONTACTS&quot;/&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.WRITE_CONTACTS&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)  // 插入联系人的姓名数据
        .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)   // 引用前面插入的原始联系人 ID
        .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) // 指定数据类型为姓名
        .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, names) // 设置联系人姓名
        .build());

ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)   // 插入联系人的电话号码数据
        .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)  // 引用前面插入的原始联系人 ID
        .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)   // 指定数据类型为电话号码
        .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, num)  // 设置电话号码
        .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)  // 设置电话号码类型为移动电话
        .build());

try {
    getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);   // 执行批量插入操作
    return true;
} catch (Exception e) {
    Log.e(TAG, &quot;insertContacts error: &quot; + e);
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;#x3C;/TabItem&gt;
&amp;#x3C;TabItem label=&quot;删除&quot;&gt;
```java
private void deleteAllContacts() {
    Uri uri = ContactsContract.Contacts.CONTENT_URI;
    String[] projection = new String[]{ContactsContract.Contacts._ID};  // 只查询联系人的 ID
    String selection = null;  // 定义查询条件，这里为空表示查询所有联系人
    String[] selectionArgs = null;
    String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + &quot; COLLATE LOCALIZED ASC&quot;;  // 按显示名称升序排序

    // 查询所有联系人的 ID
    Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
    if (cursor != null) {
        try {
            while (cursor.moveToNext()) {
                // 获取联系人的 ID 并尝试删除
                @SuppressLint(&quot;Range&quot;) long contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID));
                if (deleteContact(contactId)) {
                    Log.d(TAG, String.format(&quot;delete contact %s succ\n&quot;, contactId));
                } else {
                    Log.e(TAG, String.format(&quot;delete contact %s fail\n&quot;, contactId));
                }
            }
        } finally {
            cursor.close();
        }
    }
}

private boolean deleteContact(long contactId) {
    try {
        Uri deleteUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);  // 构建删除 URI
        int rowsDeleted = getContentResolver().delete(deleteUri, null, null);  // 执行删除操作
        if (rowsDeleted &gt; 0) {
            return true;
        }
    } catch (Exception e) {
        Log.e(TAG, &quot;deleteContactError: &quot; + e);
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;/TabItem&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;通话记录&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;uses-permission android:name=&quot;android.permission.READ_CALL_LOG&quot;/&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.WRITE_CALL_LOG&quot;/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;短信&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AndroidManifest.xml&lt;/strong&gt;中添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;uses-permission android:name=&quot;android.permission.SEND_SMS&quot; /&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.RECEIVE_SMS&quot;/&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.READ_SMS&quot; /&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.QUERY_ALL_PACKAGES&quot;
    tools:ignore=&quot;QueryAllPackagesPermission&quot; /&gt;

&amp;#x3C;application
    android:allowBackup=&quot;true&quot;
    android:dataExtractionRules=&quot;@xml/data_extraction_rules&quot;
    android:fullBackupContent=&quot;@xml/backup_rules&quot;
    android:icon=&quot;@mipmap/app&quot;
    android:label=&quot;@string/app_name&quot;
    android:roundIcon=&quot;@mipmap/app_round&quot;
    android:supportsRtl=&quot;true&quot;
    android:theme=&quot;@style/Theme.AutoFill&quot;
    &gt;
    &amp;#x3C;receiver
        android:name=&quot;.SmsReceiver&quot;
        android:permission=&quot;android.permission.BROADCAST_SMS&quot;
        android:exported=&quot;true&quot;
        tools:ignore=&quot;WrongManifestParent&quot;&gt;
        &amp;#x3C;intent-filter&gt;
            &amp;#x3C;action android:name=&quot;android.provider.Telephony.SMS_DELIVER&quot; /&gt;
        &amp;#x3C;/intent-filter&gt;
    &amp;#x3C;/receiver&gt;
    &amp;#x3C;receiver
        android:name=&quot;.MmsReceiver&quot;
        android:permission=&quot;android.permission.BROADCAST_WAP_PUSH&quot;
        android:exported=&quot;true&quot;
        tools:ignore=&quot;WrongManifestParent&quot;&gt;
        &amp;#x3C;intent-filter&gt;
            &amp;#x3C;action android:name=&quot;android.provider.Telephony.WAP_PUSH_DELIVER&quot; /&gt;
            &amp;#x3C;data android:mimeType=&quot;application/vnd.wap.mms-message&quot; /&gt;
        &amp;#x3C;/intent-filter&gt;
    &amp;#x3C;/receiver&gt;
    &amp;#x3C;service
        android:name=&quot;.HeadlessSmsSendService&quot;
        android:exported=&quot;true&quot;
        android:permission=&quot;android.permission.SEND_RESPOND_VIA_MESSAGE&quot;
        tools:ignore=&quot;WrongManifestParent&quot;&gt;
        &amp;#x3C;intent-filter&gt;
            &amp;#x3C;action android:name=&quot;android.intent.action.RESPOND_VIA_MESSAGE&quot; /&gt;
            &amp;#x3C;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;

            &amp;#x3C;data android:scheme=&quot;sms&quot; /&gt;
            &amp;#x3C;data android:scheme=&quot;smsto&quot; /&gt;
            &amp;#x3C;data android:scheme=&quot;mms&quot; /&gt;
            &amp;#x3C;data android:scheme=&quot;mmsto&quot; /&gt;
        &amp;#x3C;/intent-filter&gt;
    &amp;#x3C;/service&gt;
    &amp;#x3C;activity
        android:name=&quot;.MainActivity&quot;
        android:exported=&quot;true&quot;&gt;
        &amp;#x3C;intent-filter&gt;
            &amp;#x3C;action android:name=&quot;android.intent.action.SEND&quot; /&gt;
            &amp;#x3C;action android:name=&quot;android.intent.action.SENDTO&quot; /&gt;

            &amp;#x3C;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
            &amp;#x3C;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&gt;

            &amp;#x3C;data android:scheme=&quot;sms&quot; /&gt;
            &amp;#x3C;data android:scheme=&quot;smsto&quot; /&gt;
            &amp;#x3C;data android:scheme=&quot;mms&quot; /&gt;
            &amp;#x3C;data android:scheme=&quot;mmsto&quot; /&gt;
        &amp;#x3C;/intent-filter&gt;
        &amp;#x3C;intent-filter&gt;
            &amp;#x3C;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;
            &amp;#x3C;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
        &amp;#x3C;/intent-filter&gt;
    &amp;#x3C;/activity&gt;
&amp;#x3C;/application&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;分别创建空的&lt;strong&gt;HeadlessSmsSendService&lt;/strong&gt;，&lt;strong&gt;MmsReceiver&lt;/strong&gt;，&lt;strong&gt;SmsActivity&lt;/strong&gt;，&lt;strong&gt;SmsReceiver&lt;/strong&gt;，&lt;strong&gt;SmsService&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HeadlessSmsSendService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
public class MmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    }
}
public class SmsActivity extends Activity {
}
public class SmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    }
}
public class SmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232142.png&quot; alt=&quot;创建所须的类&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后，需要将APK设置为默认短信应用(需要手动授予)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 获取当前设备上默认的短信应用的包名
private String getDefaultSms()
{
    String packageName = &quot;null&quot;;
    Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(&quot;smsto:&quot;));
    PackageManager packageManager = getApplicationContext().getPackageManager();
    ResolveInfo resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
    if (resolveInfo != null) {
        packageName = resolveInfo.activityInfo.packageName;
    }
    return packageName;
}

// 设置当前应用为默认短信应用
private void setDefaultSms(int requestCode) {
    String defaultSmsPkg = Telephony.Sms.getDefaultSmsPackage(getApplicationContext());
    Log.d(TAG, &quot;default SMS: &quot; + defaultSmsPkg);
    if (!defaultSmsPkg.equals(getPackageName())) {
        RoleManager roleManager = (RoleManager) getSystemService(Context.ROLE_SERVICE);
        if (roleManager != null &amp;#x26;&amp;#x26; roleManager.isRoleAvailable(RoleManager.ROLE_SMS)) {
            Intent roleRequestIntent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS);
            if (roleRequestIntent != null) {
                startActivityForResult(roleRequestIntent, requestCode);  // 启动请求设置默认短信的活动
            }
        }
    }
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    // 通过判断requestCode的值来确定是哪个操作触发的结果回调
    // 默认短信应用设置结果可以在这里判断
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;while (c.moveToNext()) {
    long id = c.getLong(c.getColumnIndexOrThrow(&quot;_id&quot;));
    getContentResolver().delete(Uri.parse(&quot;content://sms/&quot; + id), null, null);
}
c.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;/TabItem&gt;
&amp;#x3C;/Tabs&gt;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.DAvHX50T.jpg"/><enclosure url="/_astro/thumbnail.DAvHX50T.jpg"/></item><item><title>获取APK相关信息</title><link>https://saneko.me/blog/09aec4e3fa4b</link><guid isPermaLink="true">https://saneko.me/blog/09aec4e3fa4b</guid><description>介绍如何在Android中获取APP相关信息，包括实现方法和常见应用场。</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前提&lt;/h2&gt;
&lt;p&gt;Android中APK部分信息通过&lt;strong&gt;adb&lt;/strong&gt;无法直接获取，但是可以通过Android中的&lt;a href=&quot;https://developer.android.com/reference/android/content/pm/ApplicationInfo&quot;&gt;ApplicationInfo&lt;/a&gt;{eos-icons:api}接口进行获取。&lt;/p&gt;
&lt;h2&gt;获取内容&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;packageName&lt;/strong&gt;：应用的包名，唯一标识每个APP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;appName&lt;/strong&gt;：应用的名称，用户在界面上看到的名字&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;isSystemApp&lt;/strong&gt;：是否为系统应用，区分预装和用户安装&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;launcherActivity&lt;/strong&gt;：应用的启动Activity类名&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;processName&lt;/strong&gt;：应用运行的进程名称&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compileSdkVersion&lt;/strong&gt;：编译时所用的SDK版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;targetSdkVersion&lt;/strong&gt;：目标适配的SDK版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;icon&lt;/strong&gt;：应用的图标数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;功能实现&lt;/h2&gt;
&lt;h3&gt;权限&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;uses-permission android:name=&quot;android.permission.QUERY_ALL_PACKAGES&quot;
    tools:ignore=&quot;QueryAllPackagesPermission&quot; /&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot;
    android:maxSdkVersion=&quot;32&quot; /&gt;
&amp;#x3C;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;
    android:maxSdkVersion=&quot;32&quot;
    tools:ignore=&quot;ScopedStorage&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MainActivity(入口)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MainActivity extends AppCompatActivity {
    private static final String TAG = &quot;MainActivity&quot;;
    private static final int REQUEST_PERMISSIONS = 1122;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        // 获取应用程序的名称和图标
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSIONS);
        }
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 权限已经被授予，可以保存应用信息
                try {
                    AppInfoManager.saveInstalledAppsInfo(MainActivity.this);
                } catch (ClassNotFoundException | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        backgroundThread.start();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSIONS) {
            if (grantResults.length &gt; 0 &amp;#x26;&amp;#x26; grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限被授予，可以保存应用信息
            } else {
                // 权限被拒绝，提示用户权限未被授予
                Log.d(TAG, &quot;权限被拒绝&quot;);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AppInfoManager（核心部分）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class AppInfoManager {
    public static String TAG = &quot;APPInfoManager&quot;;

    public static void saveInstalledAppsInfo(Context context) throws ClassNotFoundException, NoSuchMethodException {
        PackageManager packageManager = context.getPackageManager();
        List&amp;#x3C;ApplicationInfo&gt; apps = packageManager.getInstalledApplications(0);

        DatabaseHelper dbHelper = new DatabaseHelper(context);
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        for (ApplicationInfo app : apps) {
            // 获取包名
            String packageName = app.packageName;
            Log.d(TAG, app.packageName);
            String name = (String) app.loadLabel(packageManager);  // 应用的用户可见名称
            // 检查应用名是否已经存在于数据库中
            if (dbHelper.appNameExists(name)) {
                Log.d(TAG, String.format(&quot;%s exists&quot;, name));
                continue;
            }
            // 获取应用的各种标志
            int flags = app.flags;
            // 是否是系统应用
            boolean isSystemApp = (flags &amp;#x26; ApplicationInfo.FLAG_SYSTEM) != 0;
            int compileSdkVersion = app.compileSdkVersion;
            int targetSdkVersion = app.targetSdkVersion;
            // 进程名称
            String processName = app.processName;

            // 获取启动活动名
            String launcherActivity = &quot;&quot;;
            Intent intent = packageManager.getLaunchIntentForPackage(packageName);
            if (intent != null) {
                // 从Intent中获取启动Activity的组件信息
                ComponentName componentName = intent.getComponent();
                launcherActivity = componentName.getClassName(); // 启动Activity的类名
            }
            Drawable icon = app.loadIcon(packageManager);
            // 获取应用图标
            Bitmap bitmap = getBitmapFromDrawable(icon);
            // 将图标转换为可以存储到数据库中的字节数据
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
            byte[] iconBytes = baos.toByteArray();

            // 将数据保存到数据库
            ContentValues values = new ContentValues();
            values.put(&quot;packageName&quot;, packageName);
            values.put(&quot;appName&quot;, name);
            values.put(&quot;isSystemApp&quot;, isSystemApp);
            values.put(&quot;launcherActivity&quot;, launcherActivity);
            values.put(&quot;processName&quot;, processName);
            values.put(&quot;compileSdkVersion&quot;, compileSdkVersion);
            values.put(&quot;targetSdkVersion&quot;, targetSdkVersion);
            values.put(&quot;icon&quot;, iconBytes);
            db.insert(&quot;apps&quot;, null, values);
            Log.d(TAG, String.format(&quot;%s saved&quot;, name));
        }

        db.close();
        writeFinishedFlag(context);
    }

    private static Bitmap getBitmapFromDrawable(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        } else if (drawable instanceof AdaptiveIconDrawable) {
            Drawable backgroundDr = ((AdaptiveIconDrawable) drawable).getBackground();
            Drawable foregroundDr = ((AdaptiveIconDrawable) drawable).getForeground();
            // 添加非空检查
            if (backgroundDr != null &amp;#x26;&amp;#x26; foregroundDr != null) {
                Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
                Canvas canvas = new Canvas(bitmap);
                backgroundDr.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
                foregroundDr.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
                backgroundDr.draw(canvas);
                foregroundDr.draw(canvas);
                return bitmap;
            } else{
                // 创建一个完全透明的 ARGB_8888 格式的 Bitmap
                return Bitmap.createBitmap(64, 64, Bitmap.Config.ARGB_8888);
            }
        } else {
            // Fallback for other types of drawables (e.g., ColorDrawable, VectorDrawable)
            Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        }
    }

    private static void writeFinishedFlag(Context context) {
        File file = new File(Objects.requireNonNull(context.getExternalFilesDir(null)).getAbsolutePath(), &quot;result.txt&quot;);
        try (FileWriter writer = new FileWriter(file)) {
            writer.write(&quot;1&quot;);
        } catch (IOException e) {
            Log.d(TAG, &quot;writeFinishedFlag: &quot;, e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DatabaseHelper（数据保存）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = &quot;appInfo.db&quot;;
    private static final String TABLE_NAME = &quot;apps&quot;;
    private static final int DATABASE_VERSION = 1;

    private static final String CREATE_TABLE_APPS =
            &quot;CREATE TABLE &quot; + TABLE_NAME +
                    &quot;(_id INTEGER PRIMARY KEY AUTOINCREMENT, &quot; +
                    &quot;packageName TEXT, &quot; +
                    &quot;appName TEXT,&quot; +
                    &quot;isSystemApp INTEGER,&quot; +
                    &quot;launcherActivity TEXT,&quot; +
                    &quot;processName TEXT,&quot; +
                    &quot;compileSdkVersion INTEGER,&quot; +
                    &quot;targetSdkVersion INTEGER,&quot; +
                    &quot;icon BLOB);&quot;;

    public DatabaseHelper(Context context) {
        super(context, context.getExternalFilesDir(null).getAbsolutePath() +
                &quot;/&quot; + DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_APPS);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Handle database version upgrades if necessary
    }

    public boolean appNameExists(String appName) {
        SQLiteDatabase db = getReadableDatabase();
        String query = &quot;SELECT * FROM &quot; + TABLE_NAME + &quot; WHERE appName = ?&quot;;
        Cursor cursor = db.rawQuery(query, new String[]{appName});
        boolean exists = cursor.moveToFirst();
        cursor.close();
        return exists;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;安装APK&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb install -g app-debug.apk # 通过-g参数授予全部权限&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;启动APK&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb shell am start -n com.tt.appinfo/.MainActivity
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;等待获取完成&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb shell cat /sdcard/Android/data/com.tt.appinfo/files/result.txt # 结果为1时，获取完成
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;导出数据&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb pull /sdcard/Android/data/com.tt.appinfo/files/appInfo.db
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;解析数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可通过&lt;a href=&quot;https://www.strerr.com/cn/sqliteviewer.html&quot;&gt;SQLite在线查看&lt;/a&gt;{hugeicons:sql}临时查看数据
&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232352.png&quot; alt=&quot;临时查看结果&quot;&gt;
:::&lt;/p&gt;
&lt;h3&gt;python解析示例&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if __name__ == &quot;__main__&quot;:
    conn = sqlite3.connect(&apos;appInfo.db&apos;)
    cursor = conn.cursor()
    cursor.execute(&quot;SELECT _id, packageName, appName, isSystemApp, launcherActivity, processName, compileSdkVersion, targetSdkVersion, icon FROM apps&quot;)
    rows = cursor.fetchall()
    appInfos = {}
    for row in rows:
        packageName = row[1]
        appName = row[2]
        isSystemApp = row[3]
        launcherActivity = row[4]
        processName = row[5]
        compileSdkVersion = row[6]
        targetSdkVersion = row[7]
        icon = row[8]
        if packageName in appInfos.keys():
            continue
        appInfos[packageName] = {
            &quot;appName&quot;: appName,
            &quot;isSystemApp&quot;: isSystemApp,
            &quot;launcherActivity&quot;: launcherActivity,
            &quot;processName&quot;: processName,
            &quot;compileSdkVersion&quot;: compileSdkVersion,
            &quot;targetSdkVersion&quot;: targetSdkVersion,
            &quot;icon&quot;: icon,
        }

    if not os.path.exists(&quot;Icons&quot;):
        os.mkdir(&quot;Icons&quot;)

    for package in appInfos.keys():
        appName = appInfos[package][&quot;appName&quot;]
        isSystemApp = appInfos[package][&quot;isSystemApp&quot;]
        launcherActivity = appInfos[package][&quot;launcherActivity&quot;]
        processName = appInfos[package][&quot;processName&quot;]
        compileSdkVersion = appInfos[package][&quot;compileSdkVersion&quot;]
        targetSdkVersion = appInfos[package][&quot;targetSdkVersion&quot;]

        print(f&quot;[{package}]appName: {appName}&quot;)
        print(f&quot;[{package}]isSystemApp: {isSystemApp}&quot;)
        print(f&quot;[{package}]launcherActivity: {launcherActivity}&quot;)
        print(f&quot;[{package}]processName: {processName}&quot;)
        print(f&quot;[{package}]compileSdkVersion: {compileSdkVersion}&quot;)
        print(f&quot;[{package}]targetSdkVersion: {targetSdkVersion}&quot;)

        iconData = appInfos[package][&apos;icon&apos;]
        with open(f&apos;Icons/{package}.png&apos;, &apos;wb&apos;) as f:
            f.write(iconData)
        print(f&quot;{package} saved&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C#解析示例&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;private void LoadAppInfo()
{
    string connectionString = &quot;Data Source=appInfo.db;&quot;;
    using var conn = new SqliteConnection(connectionString);
    conn.Open();
    string sql = &quot;SELECT _id, packageName, appName, isSystemApp, launcherActivity, processName, compileSdkVersion, targetSdkVersion, icon FROM apps&quot;;
    using var cmd = new SqliteCommand(sql, conn);
    using (var reader = cmd.ExecuteReader())
    {
        var appInfos = new Dictionary&amp;#x3C;string, Dictionary&amp;#x3C;string, object&gt;&gt;();

        while (reader.Read())
        {
            string packageName = reader.GetString(reader.GetOrdinal(&quot;packageName&quot;));
            string appName = reader.GetString(reader.GetOrdinal(&quot;appName&quot;));
            bool isSystemApp = reader.GetBoolean(reader.GetOrdinal(&quot;isSystemApp&quot;));
            string launcherActivity = reader.GetString(reader.GetOrdinal(&quot;launcherActivity&quot;));
            string processName = reader.GetString(reader.GetOrdinal(&quot;processName&quot;));
            int compileSdkVersion = reader.GetInt32(reader.GetOrdinal(&quot;compileSdkVersion&quot;));
            int targetSdkVersion = reader.GetInt32(reader.GetOrdinal(&quot;targetSdkVersion&quot;));
            byte[]? iconData = reader.IsDBNull(reader.GetOrdinal(&quot;icon&quot;)) ? null : (byte[])reader.GetValue(reader.GetOrdinal(&quot;icon&quot;));

            if (appInfos.ContainsKey(packageName))
            {
                continue;
            }

            appInfos[packageName] = new Dictionary&amp;#x3C;string, object&gt;
                    {
                        {&quot;appName&quot;, appName},
                        {&quot;isSystemApp&quot;, isSystemApp},
                        {&quot;launcherActivity&quot;, launcherActivity},
                        {&quot;processName&quot;, processName},
                        {&quot;compileSdkVersion&quot;, compileSdkVersion},
                        {&quot;targetSdkVersion&quot;, targetSdkVersion},
                        {&quot;iconData&quot;, iconData}
                    };

            Log.Info($&quot;[{packageName}]appName: {appName}&quot;);
            Log.Info($&quot;[{packageName}]isSystemApp: {isSystemApp}&quot;);
            Log.Info($&quot;[{packageName}]launcherActivity: {launcherActivity}&quot;);
            Log.Info($&quot;[{packageName}]processName: {processName}&quot;);
            Log.Info($&quot;[{packageName}]compileSdkVersion: {compileSdkVersion}&quot;);
            Log.Info($&quot;[{packageName}]targetSdkVersion: {targetSdkVersion}&quot;);

            string iconsDir = &quot;Icons&quot;;
            if (!Directory.Exists(iconsDir))
            {
                Directory.CreateDirectory(iconsDir);
            }

            string iconPath = Path.Combine(iconsDir, $&quot;{packageName}.png&quot;);
            if (iconData != null)
            {
                File.WriteAllBytes(iconPath, iconData);
            }
            Log.Info($&quot;{packageName} saved&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.CET-GOav.jpg"/><enclosure url="/_astro/thumbnail.CET-GOav.jpg"/></item><item><title>WPF实现数据绑定</title><link>https://saneko.me/blog/7c918fe5e82e</link><guid isPermaLink="true">https://saneko.me/blog/7c918fe5e82e</guid><description>本文介绍WPF中数据绑定的实现方法，包括INotifyPropertyChanged接口、数据上下文设置及MVVM模式应用。</description><pubDate>Thu, 09 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Tabs, TabItem } from &apos;astro-pure/user&apos;;&lt;/p&gt;
&lt;h4&gt;前提&lt;/h4&gt;
&lt;p&gt;在 &lt;a href=&quot;https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/introduction-to-wpf?view=netframeworkdesktop-4.8&amp;#x26;preserve-view=true&quot;&gt;WPF&lt;/a&gt; 中，数据绑定是一种强大的机制，用于将 UI 元素（如TextBox、Button等）与数据对象进行关联。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;INotifyPropertyChanged&lt;/code&gt;接口允许数据对象在其属性值发生改变时通知 UI，从而使 UI 能够自动更新以反映最新的数据状态。&lt;/p&gt;
&lt;h3&gt;优势&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;​​自动更新 UI​​：当数据变化时，UI 自动刷新（无需手动调用 setText 或 update）&lt;/li&gt;
&lt;li&gt;​减少样板代码:​​避免手动同步数据和控件状态&lt;/li&gt;
&lt;li&gt;​支持 MVVM 模式​​：在 WPF 中，数据绑定是 MVVM（Model-View-ViewModel）的核心&lt;/li&gt;
&lt;li&gt;​​数据验证 &amp;#x26; 格式化​​：自动处理输入验证、数据转换（如日期格式化）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ToolMainWindow中实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public int Count
{
    get { return _count; }
    set { _count = value; OnPropertyChanged(nameof(Count)); }  // 通知属性值已更改
}

public ToolMainWindow()
{
    InitializeComponent();
    DataContext = this;  // 设置数据上下文
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Count++;
}

public event PropertyChangedEventHandler? PropertyChanged;  // 实现接口

protected virtual void OnPropertyChanged(string propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;#x3C;/TabItem&gt;
  &amp;#x3C;TabItem label=&quot;ToolMainWindow.xaml&quot;&gt;
```xml
&amp;#x3C;UserControl x:Class=&quot;demo.ToolMainWindow&quot;
             xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
             xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
             xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
             xmlns:local=&quot;clr-namespace:demo&quot;
             mc:Ignorable=&quot;d&quot;
             d:DataContext=&quot;{d:DesignInstance local:ToolMainWindow,
                                IsDesignTimeCreatable=False}&quot;
             d:DesignHeight=&quot;450&quot; d:DesignWidth=&quot;800&quot;&gt;
    &amp;#x3C;StackPanel&gt;
        &amp;#x3C;Button Content=&quot;click it!&quot; Click=&quot;Button_Click&quot;/&gt;
        &amp;#x3C;TextBlock Text=&quot;{Binding Count, Mode=OneWay}&quot;/&gt;
    &amp;#x3C;/StackPanel&gt;
&amp;#x3C;/UserControl&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;实现INotifyPropertyChanged接口&lt;/li&gt;
&lt;li&gt;数据上下文设置&lt;/li&gt;
&lt;li&gt;数据源设置&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ToolMainWindowViewModel中实现&lt;/h3&gt;
&lt;p&gt;适用于需要多个数据模型，方便管理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public int Count
{
    get { return _count; }
    set { _count = value; OnPropertyChanged(nameof(Count)); }  // 通知属性值已更改
}

public event PropertyChangedEventHandler? PropertyChanged;  // 实现接口

protected virtual void OnPropertyChanged(string propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;#x3C;/TabItem&gt;
  &amp;#x3C;TabItem label=&quot;ToolMainWindow.xaml.cs&quot;&gt;
```c#
public partial class ToolMainWindow : UserControl, INotifyPropertyChanged
{
    public ToolMainWindowViewModel ViewModel { get; set; }

    public ToolMainWindow()
    {
        ViewModel = new ToolMainWindowViewModel();
        InitializeComponent();
        DataContext = ViewModel;  // 设置数据上下文
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        ViewModel.Count++;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.CEcenMLK.jpg"/><enclosure url="/_astro/thumbnail.CEcenMLK.jpg"/></item><item><title>安装adb环境</title><link>https://saneko.me/blog/e1e01b0e265f</link><guid isPermaLink="true">https://saneko.me/blog/e1e01b0e265f</guid><description>本文介绍在 Windows 下安装 ADB 的步骤：从官网下载 Platform-Tools 并配置环境变量，最后验证安装是否成功。</description><pubDate>Mon, 06 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;adb（Android Debug Bridge）&lt;/code&gt;是Android开发工具包（SDK）中的一个命令行工具，用于在计算机和Android设备之间进行通信和交互。它允许开发人员安装、调试和管理Android应用程序，以及在设备和计算机之间传输文件。&lt;/p&gt;
&lt;h3&gt;安装步骤&lt;/h3&gt;
&lt;h4&gt;1. 安装SDK&lt;/h4&gt;
&lt;p&gt;从android官方网站（&lt;a href=&quot;https://developer.android.google.cn/tools/releases/platform-tools?hl=zh-cn&quot;&gt;SDK 平台工具版本说明  | Android Studio  | Android Developers&lt;/a&gt;）中下载Android SDK，可以仅下载SDK中的&lt;strong&gt;Platform-Tools&lt;/strong&gt;，Platform-Tools包含了adb与fastboot。&lt;/p&gt;
&lt;h4&gt;2. 设置环境变量&lt;/h4&gt;
&lt;p&gt;为了使系统中的任意位置都能使用adb，需要对环境变量进行设置。&lt;/p&gt;
&lt;p&gt;右键点击&lt;strong&gt;此电脑&lt;/strong&gt;或&lt;strong&gt;计算机&lt;/strong&gt;，选择&lt;strong&gt;属性&lt;/strong&gt;，进入&lt;strong&gt;高级系统设置&lt;/strong&gt;，点击&lt;strong&gt;环境变量&lt;/strong&gt;。在系统变量栏中，找到&lt;strong&gt;Path&lt;/strong&gt;并双击。&lt;/p&gt;
&lt;p&gt;点击&lt;strong&gt;新建&lt;/strong&gt;，添加&lt;strong&gt;Platform-Tools&lt;/strong&gt;路径，如下图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232230.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;保存。&lt;/p&gt;
&lt;p&gt;打开命令提示符(cmd)，输入&lt;strong&gt;adb version&lt;/strong&gt;，如果返回类似下面的结果，则表示配置成功。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;C:\Users\qiata&gt;adb version
Android Debug Bridge version 1.0.41
Version 35.0.2-12147458
Installed as C:\platform-tools\adb.exe
Running on Windows 10.0.22631
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.C3nrIlkf.jpg"/><enclosure url="/_astro/thumbnail.C3nrIlkf.jpg"/></item><item><title>CSharp执行终端指令</title><link>https://saneko.me/blog/f2c204f988b0</link><guid isPermaLink="true">https://saneko.me/blog/f2c204f988b0</guid><description>本文介绍如何用C#执行终端命令并获取输出结果，支持返回字符串和流，适用于自动化脚本和系统操作场景。</description><pubDate>Mon, 06 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;开发背景&lt;/h3&gt;
&lt;p&gt;执行终端指令，并返回结果&lt;/p&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public class NProcess
{

    /// &amp;#x3C;summary&gt;
    /// 执行cmd命令
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;command&quot;&gt;cmd命令&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;useUTF8&quot;&gt;是否使用utf-8编码&amp;#x3C;/param&gt;
    /// &amp;#x3C;returns&gt;包含命令执行结果的Process对象&amp;#x3C;/returns&gt;
    public static Process Run(string command, bool useUTF8=true)
    {
        ProcessStartInfo startInfo = new()
        {
            FileName = &quot;cmd.exe&quot;,
            Arguments = $&quot;/c {command}&quot;,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            CreateNoWindow = true, // 不显示cmd窗口
        };
        if (useUTF8)
        {
            startInfo.StandardOutputEncoding = Encoding.UTF8;
        }
        using Process process = new() { StartInfo = startInfo };
        process.Start();
        return process;
    }

    /// &amp;#x3C;summary&gt;
    /// 运行指定的命令行命令，并返回命令的输出结果。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;command&quot;&gt;要运行的命令行命令。&amp;#x3C;/param&gt;
    /// &amp;#x3C;param name=&quot;useUTF8&quot;&gt;是否使用utf-8编码。&amp;#x3C;/param&gt;
    /// &amp;#x3C;returns&gt;返回命令的输出结果。&amp;#x3C;/returns&gt;
    public static string RunReturnString(string command, bool useUTF8=true)
    {
        ProcessStartInfo startInfo = new()
        {
            FileName = &quot;cmd.exe&quot;,
            Arguments = $&quot;/c {command}&quot;,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            CreateNoWindow = true, // 不显示cmd窗口
        };
        if (useUTF8)
            startInfo.StandardOutputEncoding = Encoding.UTF8;
        using Process process = new() { StartInfo = startInfo };
        process.Start();
        // 同步读取输出
        string result = process.StandardOutput.ReadToEnd();
        // 等待进程退出
        process.WaitForExit();
        // 返回结果
        return result;
    }

    /// &amp;#x3C;summary&gt;
    /// 运行指定的命令行命令，并返回命令的输出流。
    /// &amp;#x3C;/summary&gt;
    /// &amp;#x3C;param name=&quot;command&quot;&gt;要运行的命令行命令。&amp;#x3C;/param&gt;
    /// &amp;#x3C;returns&gt;返回命令的输出流。&amp;#x3C;/returns&gt;
    public static Stream RunReturnStream(string command)
    {
        ProcessStartInfo startInfo = new()
        {
            FileName = &quot;cmd.exe&quot;,
            Arguments = $&quot;/c {command}&quot;,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            CreateNoWindow = true, // 不显示cmd窗口
        };
        using Process process = new() { StartInfo = startInfo };
        process.Start();
        var stream = process.StandardOutput.BaseStream;
        return stream;
    }

}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.CnDGlyz9.jpg"/><enclosure url="/_astro/thumbnail.CnDGlyz9.jpg"/></item><item><title>高级启动中缺少禁用驱动强制签名的解决方法</title><link>https://saneko.me/blog/3cfbd1febf0b</link><guid isPermaLink="true">https://saneko.me/blog/3cfbd1febf0b</guid><description>介绍了如何在 Windows 系统中禁用驱动强制签名，以便安装未签名的驱动程序。</description><pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;问题背景&lt;/h3&gt;
&lt;p&gt;系统高级启动选项中，有时找不到&lt;strong&gt;禁用驱动强制签名&lt;/strong&gt;的选项。&lt;/p&gt;
&lt;h3&gt;问题原因&lt;/h3&gt;
&lt;p&gt;一般来说，是由于恢复环境&lt;strong&gt;WINER.wim&lt;/strong&gt;的丢失造成的。该镜像存在于C盘根目录的Recovery文件夹，该文件夹被删除或精简。&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先Win + X 打开终端管理员&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入 &lt;code&gt;reagentc /info&lt;/code&gt; 并回车，查看WINER.wim文件是否真的丢失&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232532.png&quot; alt=&quot;恢复环境&quot;&gt;&lt;/p&gt;
&lt;p&gt;如图所示&lt;code&gt;Windows RE位置&lt;/code&gt;为空，说明文件丢失。&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;
&lt;p&gt;在系统盘根目录新建名为Recovery的文件夹，然后再在Recovery文件夹内新建名为WindowsRE的文件夹。完整路径即为&lt;strong&gt;C:\Recovery\WindowsRE&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从微软原版Windows ISO镜像中获取WinRE.wim映像，直接提取镜像中install.wim文件。或者也可以从没有丢失WINRE.wim镜像的电脑中拷贝一份。放在C:\Recovery\WindowsRE目录下。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次WIN+X 打开终端（管理员），输入&lt;strong&gt;reagentc /setreimage /path C:\Recovery\WindowsRE&lt;/strong&gt; 回车，之后再输入&lt;strong&gt;reagentc /enable&lt;/strong&gt; 回车&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;reagentc /setreimage /path C:\Recovery\WindowsRE
reagentc /enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.blog.saneko.me/Blog/blog_250116_232659.png&quot; alt=&quot;启用Windows RE&quot;&gt;&lt;/p&gt;
&lt;p&gt;操作完成后，就修复了windows的恢复环境。再进入高级启动选项中，就可以看到&quot;禁用驱动强制签名&quot;等选项。&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.Cg16Ov0M.jpg"/><enclosure url="/_astro/thumbnail.Cg16Ov0M.jpg"/></item><item><title>51单片机接收AT指令功能实现</title><link>https://saneko.me/blog/db198acafb2e</link><guid isPermaLink="true">https://saneko.me/blog/db198acafb2e</guid><description>51单片机通过串口接收AT指令（AT+USB=1/0）控制继电器，实现USB数据线通断控制的完整C语言实现方案，包含串口初始化、中断处理和指令解析代码。</description><pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;开发背景&lt;/h3&gt;
&lt;p&gt;51单片机需要接收电脑端发送的指令，来控制继电器的通断，从而控制连接电脑端的USB数据线的通断。&lt;/p&gt;
&lt;p&gt;指令配置如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;AT+USB=1  // 连接usb
AT+USB=0  // 断开usb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;功能实现&lt;/h3&gt;
&lt;p&gt;具体代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;reg52.h&gt;
#include &amp;#x3C;string.h&gt;

#define uchar unsigned char
#define uint unsigned int

uint dataCount;       // 数据计数器
uint receiveStatus;   // 接收状态：-1未开始，0错误，1开始
uchar receiveStr[10]; // 接收字符串缓冲区
// 函数声明
uint checkReceiveStatus();
void send_str_com(unsigned char *sendStr);

// 初始化串口及相关寄存器
void init()
{
    TMOD = 0x20; // 定时器1工作方式2（8位自动重装）
    PCON = 0x80; // 使能波特率倍增
    TH1 = 0xFD;  // 设置波特率为9600（根据晶振频率调整）
    TL1 = 0xFD;
    TR1 = 1; // 启动定时器1
    REN = 1; // 使能串口接收
    SM0 = 0; // 串口工作方式1（8位UART）
    SM1 = 1;
    EA = 1; // 使能全局中断
    ES = 1; // 使能串口中断
}

void main()
{
    init(); // 初始化串口
    send_str_com(&quot;init ok!&quot;);
    receiveStatus = -1; // 重置接收状态
    while (1)
    {
        if (checkReceiveStatus() == 1)
        {                             // 如果接收到完整数据
            send_str_com(receiveStr); // 回显接收到的数据

            // 根据接收到的指令执行相应操作
            if (0 == strncmp(receiveStr, &quot;AT+USB=1&quot;, 8))
            {
                send_str_com(&quot;connect usb&quot;);
                P1 = 0xff;
            }
            else if (0 == strncmp(receiveStr, &quot;AT+USB=0&quot;, 8))
            {
                send_str_com(&quot;break usb&quot;);
                P1 = 0x00;
            }
            else
            {
                receiveStatus = -1;        // 重置接收状态
                memset(receiveStr, 0, 10); // 清空接收缓冲区
            }
        }
    }
}

// 串口中断服务程序
void ser() interrupt 4 using 3
{
    if (RI)
    {           // 如果接收到数据
        RI = 0; // 清除接收中断标志
        if (receiveStatus == -1)
        { // 接收第一个数据
            if (SBUF != &apos;A&apos;)
            { // 如果不是AT指令开头报错
                receiveStatus = 0;
                dataCount = 0;
            }
            else
            {
                receiveStatus = 1; // 开始接收
                dataCount = 0;
                memset(receiveStr, 0, sizeof(receiveStr[10])); // 数据清空
                receiveStr[dataCount] = SBUF;                  // 存储第一个字符
                dataCount++;
            }
        }
        else if (receiveStatus == 1)
        { // 正在接收
            if (dataCount &gt; 10)
            {
                receiveStatus = 0;
                dataCount = 0;
            }
            else
            {
                receiveStr[dataCount] = SBUF;
                dataCount++;
            }
        }
    }
}

uint checkReceiveStatus()
{ // 检查接收状态
    if (receiveStatus == 0)
    {
        send_str_com(&quot;RECEIVE ERROR&quot;);
        receiveStatus = -1;
        return 0;
    }
    if ((dataCount &gt; 1) &amp;#x26;&amp;#x26; (receiveStatus == 1))
    { // 如果接收到完整的一行数据
        if ((receiveStr[dataCount - 2] == 0X0D) &amp;#x26;&amp;#x26; (receiveStr[dataCount - 1] == 0X0A))
        {
            receiveStatus = -1; // 重置接收状态
            return 1;           // 表示接收到完整数据
        }
    }
    return 0;
}

void send_str_com(unsigned char *sendStr) // 发送字符串
{
    uchar i;
    for (i = 0; sendStr[i] != &apos;\0&apos;; i++)
    {
        SBUF = sendStr[i];
        while (TI == 0)
            ;
        TI = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.CA9Q1j0c.jpg"/><enclosure url="/_astro/thumbnail.CA9Q1j0c.jpg"/></item></channel></rss>