<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>nmsl.cn</title>
    <link>https://nmsl.cn</link>
    <description>永远不要理会谣言和中伤</description>
    <language>zh-CN</language>
    <lastBuildDate>Fri, 14 Aug 2026 16:04:00 GMT</lastBuildDate>
    <atom:link href="https://nmsl.cn/rss.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title><![CDATA[为你的博客后台登陆加入两步验证]]></title>
      <link>https://nmsl.cn/articles/为你的博客后台登陆加入两步验证</link>
      <guid isPermaLink="true">https://nmsl.cn/articles/为你的博客后台登陆加入两步验证</guid>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[<blockquote><p>其实这个技术可以靠ai编程实现，但是我想水一篇文章，所以决定写一下本站是如何实现的,大家可以直接拿本文章给ai进行参考集成到你的网站，下面是没有依赖 `otplib` 等第三方库，服务端 + 前端完整落地记录。(其实此文也是ai拟写~)</p></blockquote><h2>为什么做这件事</h2><p>最近顺手自己做了一个博客系统，同步做了一大堆安全防护，一般大家的网站后台管理着文章、评论、等设置，一旦账号密码泄露就是"裸奔"。虽然各位在部署网站时常常会在后台登陆加入各种防线，但大多数网站只有”密码“这一道认证，验证码只是作用于人机检测和防撞库频率，意味着：密码一旦被撞库或钓鱼，攻击者就能长驱直入。<br /><strong>两步验证（2FA）</strong>的思路很简单——<u>认证的密码只有你自己可以在手机验证器app中可以看到，并且会自动刷新。</u></p><p><strong>效果如图</strong><br />后台设置页点"开启 2FA" → 认证器扫码/手动录入 → 输入 6 位码确认 → 以后每次登录输完密码还要多输一次手机上的动态码。整个过程零第三方依赖，核心算法加起来不到一百行，安全感拉满。</p><img src="https://img.alicdn.com/imgextra/i3/2448027154/O1CN01QHBcPrTAUmJ1y6e2_!!2448027154.png" /><img src="https://img.alicdn.com/imgextra/i2/2448027154/O1CN01hWfJVfeqqgJ1zMji_!!2448027154.png" /><img src="https://img.alicdn.com/imgextra/i4/2448027154/O1CN01hfam2sedCQD1roXM_!!2448027154.png" /><p>方案上我选了最通用的 TOTP（基于时间的一次性密码），也就是 <strong>Google Authenticator</strong> / <strong>Authy</strong> 那套。它不依赖短信、不需要额外发邮件，服务器和认证器各凭一个共享密钥 + 当前时间就能算出同一串 6 位数字。</p><h2>为什么要自己造轮子</h2><p>之所以手写而不是用 <mark>`otplib`</mark>，TOTP 核心算法本身只有几十行，用内置<mark> `crypto` </mark>完全可以覆盖，还能顺手把一些细节（恒定时间比较、容错时间窗）按自己的要求实现。</p><p><strong>一、TOTP 原理</strong></p><ol><li><p>服务器生成一个随机的共享密钥 `secret`（Base32 编码，方便人肉录入）；</p></li><li><p>当前 Unix 时间戳除以 `period`（30 秒）得到计数器 `counter`；</p></li><li><p>用 HMAC-SHA1 对 `counter` 做签名：`HMAC(secret, counter)`；</p></li><li><p>取 HMAC 结果的动态截断（Dynamic Truncation），得到 6 位数字；</p></li><li><p>服务器和认证器在相同时间、相同密钥下会算出**完全相同**的 6 位数字。</p></li></ol><p>因为算法是公开的，用户只需要在认证器 App 里录入密钥（扫码或手动输入），之后 App 每 30 秒生成一次验证码。</p><img src="https://img.alicdn.com/imgextra/i3/2448027154/O1CN01SI38vJrcM9J2BCQ5_!!2448027154.jpg" /><p><strong>二、数据库：两个新字段</strong></p><p><mark>`AdminUserEntity` </mark>增加两个字段，<mark>`totp_secret`</mark> 允许为空，<mark>`totp_enabled`</mark> 默认关闭：</p><pre class="pure-highlightjs"><code class="language-typescript">@Column('varchar', { name: 'totp_secret', length: 255, nullable: true })
totpSecret: string | null;

@Column('tinyint', { name: 'totp_enabled', default: 0 })
totpEnabled: boolean;</code></pre><p>注意一个细节：<mark>`secret`</mark> 和 <mark>`enabled` </mark>是<strong>分开存储</strong>的，这正好支撑了"<strong>先扫码、后启用</strong>"的流程——密钥可以先存着，但开关始终是关闭的，直到用户用验证码确认过才置为<mark> `true`</mark>。</p><p><strong>三、服务端：手写 TOTP 工具类</strong></p><pre class="pure-highlightjs"><code class="language-typescript">const STEP_SECONDS = 30;
const DIGITS = 6;
const WINDOW = 1; // 容错 ±1 个时间窗
/** 生成 32 位大写 Base32 密钥 */
export function generateTotpSecret(): string {
  return base32Encode(crypto.randomBytes(20));
}
/** 生成某个时刻的 TOTP（动态截断） */
function generateTotp(secret: string, offset = 0): string {
  const counter = Math.floor(Date.now() / 1000 / STEP_SECONDS) + offset;
  const buffer = Buffer.alloc(8);
  buffer.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
  buffer.writeUInt32BE(counter &gt;&gt;&gt; 0, 4);

  const hmac = crypto.createHmac('sha1', base32Decode(secret)).update(buffer).digest();
  const offsetByte = hmac[hmac.length - 1] &amp; 0x0f;
  const binary =
    ((hmac[offsetByte] &amp; 0x7f) &lt;&lt; 24) |
    ((hmac[offsetByte + 1] &amp; 0xff) &lt;&lt; 16) |
    ((hmac[offsetByte + 2] &amp; 0xff) &lt;&lt; 8) |
    (hmac[offsetByte + 3] &amp; 0xff);
  return (binary % 10 ** DIGITS).toString().padStart(DIGITS, '0');
}</code></pre><p><strong>几个值得说的点：</strong></p><ul><li><p>Base32 编码/解码也是自己实现的（`base32Decode` / `base32Encode`），Google Authenticator 的密钥就是 Base32 字符集 `A-Z2-7`；</p></li><li><p>计数器对齐：时间戳先用 `Math.floor(Date.now() / 1000 / 30)` 对齐到 30 秒窗口，再拆成 8 字节大端序——注意 `writeUInt32BE` 每次只能写 4 字节，所以拆成高 32 位和低 32 位两次写入，避免 JS 数字精度问题；</p></li><li><p>动态截断：取 HMAC 结果最后一字节的低 4 位作为偏移量，从这个偏移位置取 4 字节，高位清零后对 `10^6` 取模，不足 6 位补零。</p></li></ul><p>验证时，为了让网络延迟或手机时间轻微偏差不至于导致验证失败，我会在 `±WINDOW`（±1 个时间窗）内各算一次来比对：</p><pre class="pure-highlightjs"><code class="language-typescript">export function verifyTotp(secret: string, code: string): boolean {
  const cleaned = (code || '').replace(/\s+/g, '');
  if (!/^\d{6}$/.test(cleaned)) return false;
  for (let offset = -WINDOW; offset &lt;= WINDOW; offset++) {
    if (constantTimeEqual(generateTotp(secret, offset), cleaned)) return true;
  }
  return false;
}</code></pre><p>验证码比对用了<mark>恒定时间比较</mark>，防止通过响应耗时来推断正确码位。另外还生成 <mark>`otpauth://` </mark>链接，方便认证器扫码直接添加：</p><pre class="pure-highlightjs"><code class="language-typescript">export function buildOtpauthUrl(secret: string, account: string, issuer = 'nmsl'): string {
  const params = new URLSearchParams({
    secret, issuer,
    algorithm: 'SHA1', digits: '6', period: '30',
  });
  return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(account)}?${params.toString()}`;
}</code></pre><p><strong>四、服务端：登录校验接入</strong></p><p>在<mark> `AuthService.login` </mark>里，密码校验通过后，如果用户已启用 2FA，就强制校验动态码：</p><pre class="pure-highlightjs"><code class="language-typescript">const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!isPasswordValid) {
  throw new UnauthorizedException('用户名或密码错误');
}

// 2FA 校验：已启用时强制要求动态口令
if (user.totpEnabled &amp;&amp; user.totpSecret) {
  if (!dto.totpCode) {
    throw new UnauthorizedException('请输入 2FA 动态验证码');
  }
  if (!verifyTotp(user.totpSecret, dto.totpCode)) {
    throw new UnauthorizedException('2FA 验证码错误');
  }
}</code></pre><p>这里有个刻意的顺序：<em>先校验密码，再校验 2FA</em>。这样即使 2FA 报错，也不会把"密码对不对"泄露给攻击者；前端则可以凭借错误消息里是否含 `2FA` 来决定要不要弹出验证码输入框。</p><p><mark>`LoginDto`</mark> 里<mark> `totpCode` </mark>是可选的，但要允许空串通过校验（因为前端在用户没启用 2FA 时不会传这个字段）：</p><pre class="pure-highlightjs"><code class="language-typescript">@ValidateIf((_o, value) =&gt; value !== undefined &amp;&amp; value !== null &amp;&amp; value !== '')
@IsString()
@MinLength(6)
@MaxLength(6)
totpCode?: string;</code></pre><p><strong>五、服务端：开启 / 关闭接口</strong><br />三个接口都挂在 `/auth` 下，全部需要 JWT 登录态，并且加了接口级限流：</p><ul><li><p>`POST /auth/2fa/setup` | 生成密钥并返回 `secret` + `otpauthUrl` | 10 次/分钟</p></li><li><p>`POST /auth/2fa/verify` | 校验验证码并正式启用 | 5 次/分钟</p></li><li><p>`POST /auth/2fa/disable` | 校验验证码后关闭 | 5 次/分钟</p></li></ul><p><strong>setup：只生成，不启用</strong></p><pre class="pure-highlightjs"><code class="language-typescript">async setupTwoFactor(userId: number, account: string) {
  const user = await this.adminUserRepository.findOne({ where: { id: userId } });
  if (user.totpEnabled) {
    throw new BadRequestException('2FA 已启用，如需重置请先关闭');
  }

  const secret = generateTotpSecret();
  await this.adminUserRepository.update(userId, {
    totpSecret: secret,
    totpEnabled: false, // 关键：先不启用
  });

  return {
    code: 0, message: 'ok',
    data: { secret, otpauthUrl: buildOtpauthUrl(secret, account || user.username, 'nmsl') },
  };
}</code></pre><p><mark>`secret`</mark> 只在这个响应里出现一次，之后服务器不再回传明文（这也是为什么要让用户立即去录入）。<br /><strong>verify：确认后真正开启</strong></p><pre class="pure-highlightjs"><code class="language-typescript">async verifyTwoFactor(userId: number, code: string) {
  const user = await this.adminUserRepository.findOne({ where: { id: userId } });
  if (!user || !user.totpSecret) throw new BadRequestException('请先生成 2FA 密钥');
  if (user.totpEnabled) throw new BadRequestException('2FA 已启用');
  if (!verifyTotp(user.totpSecret, code)) throw new BadRequestException('验证码错误');

  await this.adminUserRepository.update(userId, { totpEnabled: true });
  return { code: 0, message: 'ok', data: { totpEnabled: true } };
}</code></pre><p>这个"先生成密钥 → 验证成功才启用"的两段式设计很有用：如果用户在启用前把密钥弄丢了，<mark>`totpEnabled` </mark>仍为 <mark>`false`</mark>，登录不会被锁死，重新生成密钥即可。<br /><br /><strong>disable：关 2FA 也要验证码</strong></p><pre class="pure-highlightjs"><code class="language-typescript">async disableTwoFactor(userId: number, code: string) {
const user = await this.adminUserRepository.findOne({ where: { id: userId } });
if (!user || !user.totpSecret) throw new BadRequestException('2FA 未启用');
if (!verifyTotp(user.totpSecret, code)) throw new BadRequestException('验证码错误');

await this.adminUserRepository.update(userId, {
totpSecret: null,
totpEnabled: false,
});
return { code: 0, message: 'ok', data: { totpEnabled: false } };
}</code></pre><p>关闭 2FA 同样要验证码——否则攻击者只要拿到登录态，就能一键把用户的安全防线关掉。<br /><br /><strong>限流</strong><br />登录和 2FA 相关接口都用 <mark>`@Throttle` </mark>做了限流，防止验证码被暴力穷举（6 位数字只有 100 万种组合，不限制后果严重）：</p><pre class="pure-highlightjs"><code class="language-typescript">@Post('2fa/verify')
@UseGuards(JwtAuthGuard)
@Throttle({ default: { limit: 5, ttl: 60_000 } })
@ApiOperation({ summary: '验证并启用 2FA' })
async verifyTwoFactor(@Request() req: any, @Body() dto: VerifyCodeDto) {
return this.authService.verifyTwoFactor(req.user.sub, dto.code);
}</code></pre><p><strong>六、前端：登录页</strong><br />登录页的关键交互是：<strong>用户没有启用 2FA 时，页面只有用户名 + 密码两个输入框；一旦服务端返回"请输入 2FA"之类的错误，才动态弹出验证码输入框</strong>。这样大多数登录流程保持简洁，也避免"密码错误"和"验证码错误"被混为一谈。</p><p><strong>七、前端：后台设置面板</strong></p><p><mark>`TwoFactorPanel` </mark>组件放在"账户安全（2FA）"设置区块里，一个组件管三种状态：<br /><br /><strong>1. 未启用</strong> → 一个"开启 2FA"按钮，点击后调<mark> `/auth/2fa/setup`</mark>：<br /><strong>2. 已生成密钥待确认 </strong>→ 展示<mark> `secret`</mark>（等宽字体、<mark>`userSelect: 'all'`</mark> 方便全选复制）和 <mark>`otpauthUrl` </mark>链接，用户用认证器录入后，输入 6 位验证码点击"确认启用"，调 <mark>`/auth/2fa/verify`</mark>：<br /><strong>3. 已启用 </strong>→ 显示"已启用"状态，底部留一个验证码输入框 + "关闭 2FA"按钮，关闭前 <mark>`window.confirm`</mark> 二次确认。</p><pre class="pure-highlightjs"><code>const handleVerify = async () =&gt; {
if (!/^\d{6}$/.test(code.trim())) {
setMessage({ type: 'error', text: '请输入 6 位动态验证码' });
return;
}
setBusy(true);
setMessage(null);
try {
await api.post('/auth/2fa/verify', { code: code.trim() });
setEnabled(true);
setSetup(null);
setCode('');
setMessage({ type: 'success', text: '2FA 已启用' });
} catch (err) {
setMessage({ type: 'error', text: err instanceof Error ? err.message : '验证失败' });
} finally {
setBusy(false);
}
};</code></pre><p>面板挂载时通过 <mark>`GET /auth/me` </mark>读取 <mark>`totpEnabled` </mark>来初始化状态，加载态用 <mark>`loading`</mark> 兜底，所有失败路径都打了日志（<mark>`logDataError`</mark>）。</p><h2>踩坑与思考</h2><p><strong>1. 为什么 secret 在启用前先存库？</strong>为了两段式启用。如果只在 verify 时生成并返回，用户扫码的时机和启用是耦合的；分开存可以随时重新生成，不怕半路丢密钥。<br /><strong>2. 时间同步问题：</strong>TOTP 强依赖设备时钟，认证器手机时间偏差超过一个时间窗就会失败。我只开了 `±1` 容错，够用但不算宽。真要更稳可以按偏移量留存验证记录、动态放宽窗口（但这会引入新的复杂度，个人博客没必要）。<br /><strong>3. 没有做恢复码（backup code）</strong>：一旦手机丢失且密钥未备份，2FA 会把自己锁在门外。个人博客场景下，我倾向于留一条人工降级通道（比如直接改库），所以没有专门做恢复码机制。如果面向多用户，恢复码几乎是必须的。<br /><strong>4. 限流是 2FA 的"另一条腿"：</strong>6 位数字空间只有 100 万，没有限流等于把门虚掩着。登录、验证、关闭三个入口我都压到了 5–20 次/分钟。<br /><strong>5. `VerifyCodeDto` 用正则 `^\d{6}$` 强校验</strong>，非法输入在 DTO 层就被挡掉，<mark>`verifyTotp` </mark>内部也做了二次防御，双保险。</p>]]></content:encoded>
      <pubDate>Fri, 14 Aug 2026 12:30:00 GMT</pubDate>
      
    </item>
    
    <item>
      <title><![CDATA[成功上车EdgeOne腾讯免费cdn和wp显示评论ip归属地]]></title>
      <link>https://nmsl.cn/articles/成功上车edgeone腾讯免费cdn和wp显示评论ip归属地</link>
      <guid isPermaLink="true">https://nmsl.cn/articles/成功上车edgeone腾讯免费cdn和wp显示评论ip归属地</guid>
      <description><![CDATA[最近腾讯正在内测边缘安全加速平台 EdgeOne，自称是目前全球唯一同时支持国际和国内加速的免费 CDN 平台，当然好像的确如此！感谢博主hzlzh的兑换码赠送，目前本站已经配置完毕(虽然我也不确定配置的对不对...)...]]></description>
      <content:encoded><![CDATA[<img src="https://img.alicdn.com/imgextra/i3/2448027154/O1CN01cDfvtB22iaWLmTfbH_!!2448027154.jpg"><p>最近腾讯正在内测边缘安全加速平台 EdgeOne， 自称是目前全球唯一同时支持国际和国内加速的免费 CDN 平台， 当然好像的确如此！ 目前本站已经配置完毕(虽然我也不确定配置的对不对...)</p><p>因为没有套cdn时服务器就是本省区域的，所以对我来说是感知不强， 不知道其他省份网友们访问时是否有感觉的比之前要快了呢...</p><p>感兴趣的小伙伴可以去获取兑换码体验一番！ </p><p>官网：<a target="_blank" rel="noopener noreferrer nofollow" href="https://edgeone.ai/redemption">https://edgeone.ai/redemption</a></p><img src="https://img.alicdn.com/imgextra/i4/2448027154/O1CN01T17cgU22iaWJxsI11_!!2448027154.jpg"><p><span style="color: rgb(255, 0, 0);"><strong>另外使用wordpress的博主注意！</strong></span> 使用cdn后，假如评论区有设置ip归属地， 请在腾讯云cdn设置界面开启:<strong>携带客户端 IP 功能</strong></p><img src="https://img.alicdn.com/imgextra/i1/2448027154/O1CN01hJUv0N22iaWG1h4Yg_!!2448027154.jpg"><p> 在wordpress根目录的<code>wp-config.php</code> 或主题的 <code>functions.php</code> 文件中添加以下代码: </p><pre><code class="language-php">if (isset($_SERVER['HTTP_EO_CLIENT_IP'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_EO_CLIENT_IP'];
}
</code></pre><p> 至此，用户再次评论时，才会显示正确来源的ip归属地！</p><img src="https://img.alicdn.com/imgextra/i2/2448027154/O1CN01zEhscT22iaWKO7x27_!!2448027154.jpg">]]></content:encoded>
      <pubDate>Fri, 20 Jun 2025 04:05:00 GMT</pubDate>
      
    </item>
    
    <item>
      <title><![CDATA[三星Galaxy fit3和bsc100s码表适配成功！]]></title>
      <link>https://nmsl.cn/articles/三星galaxy-fit3和bsc100s码表适配成功</link>
      <guid isPermaLink="true">https://nmsl.cn/articles/三星galaxy-fit3和bsc100s码表适配成功</guid>
      <description><![CDATA[去年买的fit3手环一直没有和码表的心率配对成功于是重置手环连接了三星手机，升级了固件，并且把码表也升级了最新固件，以下是多次尝试多次成功的步骤：1.关闭手机蓝牙，为的是断开手环和码表。这很重要！必须关闭手机蓝牙！2.长按码表右键，切换到e3模式，【重点】这时候可以在e3模式下点按左键，会保持在显示三个数值为“--”的状态...]]></description>
      <content:encoded><![CDATA[<div class="ai-optimize-introduction">去年买的fit3手环一直没有和码表的心率配对成功</div>
<div>于是重置手环连接了三星手机，升级了固件，</div>
<div>并且把码表也升级了最新固件，</div>
<div>以下是多次尝试多次成功的步骤：</div>
<div></div>
<div>1.关闭手机蓝牙，为的是断开手环和码表。</div>
<div>这很重要！必须关闭手机蓝牙！</div>
<div></div>
<div>2.长按码表右键，切换到e3模式，</div>
<div>【重点】这时候可以在e3模式下点按左键，</div>
<div>会保持在显示三个数值为“--”的状态，</div>
<div></div>
<div>3.一旦检测到手环心率就会显示心率数值并自动回到默认码表桌面，反正我是这样搞的，</div>
<div></div>
<div>【重点】</div>
<div>在等待心率数值显示过程中，点按码表左键，避免码表自动回到默认桌面！</div>
<div></div>
<div>连接成功后手环连接手机蓝牙好像连接不上，手环不能同时连接手机和码表。</div>
<div></div>
<div>实测在实时检测模式下的心率和码表心率延迟很低！！！</div>
<div></div>
<div>希望可以能够帮助到遇到有相同设备有相同问题的人。</div>
<div><img class="alignnone size-medium" src="https://img.alicdn.com/imgextra/i4/2448027154/O1CN01tfNahn22iaVY2WRlE_!!2448027154.jpg" width="1064" height="1064" /></div>]]></content:encoded>
      <pubDate>Sat, 03 May 2025 03:12:22 GMT</pubDate>
      
    </item>
    
    <item>
      <title><![CDATA[简单记录go实现窗口吸附]]></title>
      <link>https://nmsl.cn/articles/简单记录go实现窗口吸附</link>
      <guid isPermaLink="true">https://nmsl.cn/articles/简单记录go实现窗口吸附</guid>
      <description><![CDATA[简单记录go语言实现windows桌面应用窗口吸附，类似于“快捷短语软件”可吸附在聊天软件的窗口右侧进行吸附跟随移动..
使用Wails框架实现桌面应用开发，通过Go的syscall和windows包直接调用Windows API...]]></description>
      <content:encoded><![CDATA[<blockquote>
<div>简单记录go语言实现windows桌面应用窗口吸附，类似于“快捷短语软件”可吸附在聊天软件的窗口右侧进行吸附跟随移动..</div></blockquote>
<div class="ai-optimize-introduction">使用Wails框架实现桌面应用开发，通过Go的syscall和windows包直接调用Windows API</div>
<h3 class="ai-optimize-6">使用了user32.dll中的多个API：</h3>
<ul>
 	<li class="ai-optimize-7">
<div>EnumWindows - 枚举所有顶层窗口</div></li>
 	<li class="ai-optimize-8">
<div>GetWindowTextW - 获取窗口标题</div></li>
 	<li class="ai-optimize-9">
<div>GetWindowRect - 获取窗口位置和大小</div></li>
 	<li class="ai-optimize-10">
<div>IsWindowVisible - 检查窗口是否可见</div></li>
</ul>
<div>使用golang.org/x/sys/windows包进行系统调用，在StartAttach方法中使用了goroutine持续跟踪窗口位置，实现了窗口枚举、标题获取、位置跟踪等功能 ◦ 可以附加到其他窗口并跟随其移动。</div>
<div></div>
<h3 class="ai-optimize-11">效果gif图：</h3>
<div><img class="alignnone size-medium" src="https://img.alicdn.com/imgextra/i4/2448027154/O1CN01Qm3Yvz22iaV2nJiUl_!!2448027154.gif" width="1403" height="792" /></div>
<h3 class="ai-optimize-12">app.go文件代码：</h3>
<pre class="pure-highlightjs line-numbers"><code class="language-none">package main

import (
"context"
"syscall"
"time"
"unsafe"
"github.com/wailsapp/wails/v2/pkg/runtime" // Wails 框架提供，用于操作窗口等
"golang.org/x/sys/windows"                 // 提供对 Windows 系统调用的支持
)

// 窗口的矩形区域
type RECT struct {
Left   int32 // 矩形左边界
Top    int32 // 矩形上边界
Right  int32 // 矩形右边界
Bottom int32 // 矩形下边界
}

// 定义 Windows 系统相关函数的变量
var (
user32              = windows.NewLazySystemDLL("user32.dll") // 加载 user32.dll，用于调用 Windows 窗口相关函数
enumWindowsProc     = user32.NewProc("EnumWindows")          // 枚举窗口函数
getWindowTextProc   = user32.NewProc("GetWindowTextW")       // 获取窗口标题函数
getWindowRectProc   = user32.NewProc("GetWindowRect")        // 获取窗口矩形区域函数
isWindowVisibleProc = user32.NewProc("IsWindowVisible")      // 判断窗口是否可见函数
)

// 存储窗口信息
type WindowInfo struct {
Handle int    // 窗口句柄
Title  string // 窗口标题
}

// 存储应用程序的状态
type App struct {
ctx        context.Context // 上下文
targetHWND uintptr         // 目标窗口句柄
running    bool            // 是否正在运行
}

// 创建一个新的 App 实例
func NewApp() *App {
return &amp;App{}
}

// Startup 方法，在应用程序启动时调用
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx // 设置上下文
}

// GetWindows 方法，获取所有可见窗口的信息
func (a *App) GetWindows() []WindowInfo {
var windows []WindowInfo // 用于存储窗口信息的切片
// 定义回调函数，用于枚举窗口时调用
cb := syscall.NewCallback(func(hwnd uintptr, _ uintptr) uintptr {
if isWindowVisible(hwnd) { // 判断窗口是否可见
title := getWindowText(hwnd) // 获取窗口标题
if len(title) &gt; 0 {          // 如果标题不为空
windows = append(windows, WindowInfo{ // 将窗口信息添加到切片中
Handle: int(hwnd),
Title:  title,
})
}
}
return 1 // 返回 1 表示继续枚举
})
// 调用 EnumWindows 函数枚举窗口
enumWindowsProc.Call(cb, 0)
return windows // 返回窗口信息切片
}

// StartAttach 方法，开始附加到目标窗口
func (a *App) StartAttach(hwnd int) {
a.targetHWND = uintptr(hwnd) // 设置目标窗口句柄
a.running = true             // 设置为正在运行

// 实时更新窗口位置和大小
go func() {
for a.running { // 当正在运行时
if rect, err := getWindowRect(a.targetHWND); err == nil { // 获取目标窗口矩形区域
// 分两步设置位置和大小
runtime.WindowSetPosition(a.ctx, int(rect.Right), int(rect.Top)) // 设置窗口位置
runtime.WindowSetSize(a.ctx, 300, int(rect.Bottom-rect.Top))     // 设置窗口大小
}
time.Sleep(100 * time.Millisecond) // 每 100 毫秒更新一次
}
}()
}

// StopAttach 方法，停止附加到目标窗口
func (a *App) StopAttach() {
a.running = false // 设置为停止运行
}

// getWindowRect 函数，获取窗口的矩形区域
func getWindowRect(hwnd uintptr) (*RECT, error) {
var rect RECT                          // 创建 RECT 结构体实例
ret, _, err := getWindowRectProc.Call( // 调用 GetWindowRect 函数
hwnd,
uintptr(unsafe.Pointer(&amp;rect)),
)
if ret == 0 { // 如果返回值为 0，表示失败
return nil, err
}
return &amp;rect, nil // 返回矩形区域和 nil 错误
}

// isWindowVisible 函数，判断窗口是否可见
func isWindowVisible(hwnd uintptr) bool {
ret, _, _ := isWindowVisibleProc.Call(hwnd) // 调用 IsWindowVisible 函数
return ret != 0                             // 如果返回值不为 0，表示窗口可见
}

// getWindowText 函数，获取窗口标题
func getWindowText(hwnd uintptr) string {
var text [512]uint16    // 创建一个足够大的缓冲区存储标题
getWindowTextProc.Call( // 调用 GetWindowText 函数
hwnd,
uintptr(unsafe.Pointer(&amp;text[0])),
uintptr(len(text)),
)
return syscall.UTF16ToString(text[:]) // 将 UTF-16 编码的字符串转换为 Go 的字符串
}</code></pre>]]></content:encoded>
      <pubDate>Thu, 03 Apr 2025 03:04:56 GMT</pubDate>
      
    </item>
    
    <item>
      <title><![CDATA[关于安装PyQt6成功,安装PyQt6tools报错]]></title>
      <link>https://nmsl.cn/articles/关于安装pyqt6成功安装pyqt6tools报错</link>
      <guid isPermaLink="true">https://nmsl.cn/articles/关于安装pyqt6成功安装pyqt6tools报错</guid>
      <description><![CDATA[解决方案：先用python3.11的插件包进行安装，有了环境，就可以正常安装pyqt6-tools了。pyqt6的插件下载地址：https://pypi.org/project/pyqt6-plugins/#files
下载后的文件名为：pyqt6_plugins-6.4.2.2.3-cp311-cp311-win_amd64.whl...]]></description>
      <content:encoded><![CDATA[<strong>解决方案：</strong>
先用python3.11的插件包进行安装，有了环境，就可以正常安装pyqt6-tools了。
pyqt6的插件下载地址：<a href="https://pypi.org/project/pyqt6-plugins/#files">https://pypi.org/project/pyqt6-plugins/#files</a>
下载后的文件名为：
<strong>pyqt6_plugins-6.4.2.2.3-cp311-cp311-win_amd64.whl</strong>
把文件拖到WinRAR解压缩文件中打开，找到：
<strong>pyqt6_plugins-6.4.2.2.3.dist-info/METADATA</strong>
把“<strong>METADATA</strong>”文件移出压缩包，用记事本打开进行修改。
删除：<del>（==6.4.2）</del>，
然后保存，
拖进原来的压缩包，替换之前的“<strong>METADATA</strong>”文件。
更改压缩包名称为：<strong>pyqt6_plugins-6.4.2.2.3-py3-none-any.whl</strong>，
把压缩包放进PyQt6的安装路径中：\Python312\Lib\site-packages\PyQt6
在该路径上打开终端进行安装，
<pre class="pure-highlightjs line-numbers"><code class="language-python">pip install</code></pre>
可以在尾部加上清华的镜像，提高下载速度。
加上清华源的pip安装命令为：
<pre class="pure-highlightjs line-numbers"><code class="language-python">pip install C:\Users\Administrator\AppData\Local\Programs\Python\Python312\Lib\site-packages\PyQt6\pyqt6_plugins-6.4.2.2.3-py3-none-any.whl -i https://pypi.tuna.tsinghua.edu.cn/simple/</code></pre>
安装成功以后再安装pyqt6-tools就可以成功安装了
<pre class="pure-highlightjs line-numbers"><code class="language-python">pip install pyqt6-tools -i https://pypi.tuna.tsinghua.edu.cn/simple/</code></pre>]]></content:encoded>
      <pubDate>Tue, 04 Mar 2025 13:24:42 GMT</pubDate>
      
    </item>
    
    <item>
      <title><![CDATA[三星S23/S24系列国行港台版刷机教程]]></title>
      <link>https://nmsl.cn/articles/三星s23-s24系列国行港台版刷机教程</link>
      <guid isPermaLink="true">https://nmsl.cn/articles/三星s23-s24系列国行港台版刷机教程</guid>
      <description><![CDATA[1.第一步安装手机连接电脑的usb驱2.第二步安装环境以免下载刷机包出下载并解压下载刷机包的软...]]></description>
      <content:encoded><![CDATA[<p>1.第一步安装手机连接电脑的usb驱动<br /><a href="https://wwk.lanzouj.com/iYOgj228qpcb" rel="noopener noreferrer nofollow" target="_blank">https://wwk.lanzouj.com/iYOgj228qpcb</a><br />2.第二步安装环境以免下载刷机包出错<br /><a href="https://wwk.lanzouj.com/iQZfQ228qqsd" rel="noopener noreferrer nofollow" target="_blank">https://wwk.lanzouj.com/iQZfQ228qqsd</a><br />3.下载并解压下载刷机包的软件<br /><a href="https://wwk.lanzouj.com/iLA2M228rayj" rel="noopener noreferrer nofollow" target="_blank">https://wwk.lanzouj.com/iLA2M228rayj</a><br />打开软件如图所示：<br /><img src="https://img.alicdn.com/imgextra/i1/2448027154/O1CN01Xw5Vo822iaVAdmCDR_!!2448027154.jpg" width="950" height="620" /><br />填写手机的型号，还有地区，还有序列号<br />地区代码如下：</p>



<ul>
<li>刷国行填：<strong>CHC</strong></li>



<li>刷台版填：<strong>BRI</strong></li>



<li>刷港版填：<strong>TGY</strong></li>
</ul>



<p>然后点击查找，查找后会出现下载信息 点击下载即可。<br />4.将下载的刷机包解压出来，会出现很多文件，我们只需要图2这四个文件<br /><img src="https://img.alicdn.com/imgextra/i4/2448027154/O1CN01OlM3CY22iaVCJUxAP_!!2448027154.jpg" width="752" height="364" /><br />5.下载并解压刷机工具<br /><a href="https://wwk.lanzouj.com/iKGAf228rn1e" rel="noopener noreferrer nofollow" target="_blank">https://wwk.lanzouj.com/iKGAf228rn1e</a><br />6.把手机重要文件都备份好，手机充到大约50%电量，<br /><strong>解除屏幕密码</strong>以及退出<strong>三星账户</strong>，重置手机恢复出厂设置！<br />用usb数据线连接电脑和手机，手机关机，<br /><strong>同时按音量上下键</strong>，2个键一起按，手机会绿屏出现一些韩文加中文，这时再按音量上键进入刷机模式，<br />如图3刷机工具odin软件会提示手机连接成功！<br /><img src="https://img.alicdn.com/imgextra/i1/2448027154/O1CN0111d5Id22iaV8IOAH9_!!2448027154.jpg" width="641" height="316" /><br />将步骤3下载的刷机包的那四个文件拖进刷机工具，然后点击start开始刷机。<br /><img src="https://img.alicdn.com/imgextra/i3/2448027154/O1CN01I7A5Nw22iaVB30tqP_!!2448027154.jpg" width="895" height="480" /><br /><img src="https://img.alicdn.com/imgextra/i4/2448027154/O1CN01wF2QPB22iaVAdnXKm_!!2448027154.jpg" width="867" height="564" /><br />8.如图6所示，显示绿色pass即刷机成功，可以开机使用了。<br /><img src="https://img.alicdn.com/imgextra/i2/2448027154/O1CN01nwfH5c22iaVA6nD8K_!!2448027154.jpg" width="827" height="574" /></p>]]></content:encoded>
      <pubDate>Fri, 07 Jun 2024 06:24:00 GMT</pubDate>
      
    </item>
    
  </channel>
</rss>