<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://www.caritasem.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.caritasem.com/" rel="alternate" type="text/html" /><updated>2026-06-03T10:49:18+00:00</updated><id>http://www.caritasem.com/feed.xml</id><title type="html">caritasem’s blog</title><subtitle>blog to record know-how and thoughts bit by bit
</subtitle><author><name>caritasem</name><email>caritasem@gmail</email></author><entry><title type="html">JupyterHub 进阶实践：集成 Azure AD 实现企业级单点登录 (二期)</title><link href="http://www.caritasem.com/2026/05/JupyterHub-and-Azure-AD-step-by-step/" rel="alternate" type="text/html" title="JupyterHub 进阶实践：集成 Azure AD 实现企业级单点登录 (二期)" /><published>2026-05-17T02:00:00+00:00</published><updated>2026-05-17T02:00:00+00:00</updated><id>http://www.caritasem.com/2026/05/JupyterHub-and-Azure-AD-step-by-step</id><content type="html" xml:base="http://www.caritasem.com/2026/05/JupyterHub-and-Azure-AD-step-by-step/"><![CDATA[<h2 id="引言">引言</h2>

<p>在一期部署中，我们成功搭建了基于 SystemdSpawner 与本地 PAM 认证的多用户 JupyterHub。随着团队规模的扩大，手动管理 Linux 用户、维护密码不仅繁琐，而且无法满足企业对单点登录（SSO）与多因素认证（MFA）的安全合规要求。</p>

<p>本期我们将 JupyterHub 升级为 <strong>Azure AD（Microsoft Entra ID）OAuth2 认证</strong>。由于一期已经严格遵循了统一的用户名规范，本次升级无需迁移已有用户数据，可以实现无痛过渡。</p>

<!--more-->

<h2 id="二期架构与认证时序">二期架构与认证时序</h2>

<h3 id="21-网络架构">2.1 网络架构</h3>

<p>在二期架构中，为支持 OAuth2 回调，引入了应用负载均衡器（ALB）并配置公网域名：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>内网用户
  │
  ▼
http://jupyter.x.com  （内网 DNS，HTTP，80）
  │  内网 DNS 解析到 JupyterHub 实例内网 IP
  ▼
JupyterHub 实例
  │
  ▼  OAuth 认证跳转与回调
https://jupyter-callback.x.com  （公网 ALB，HTTPS，443）
  │  ALB 负责终止 TLS 并转发流量
  ▼
JupyterHub 服务（0.0.0.0:8000）
  │
  ├─ 认证：Azure AD OAuth2（用户生命周期完全托管于 Azure 侧）
  │
  └─ Spawner：SystemdSpawner
     └─ 用户首次登录 → 自动创建 Linux 系统用户 → 独立 Systemd 实例
</code></pre></div></div>

<h3 id="22-认证时序">2.2 认证时序</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 用户访问 http://jupyter.x.com
         │
2. JupyterHub 发起 302 重定向到 Azure AD 登录页面
         │  带上参数 redirect_uri=https://jupyter-callback.x.com/hub/oauth_callback
         │
3. 用户在 Azure AD 侧进行身份验证（如 MFA 校验）
         │
4. Azure AD 认证通过，重定向回公网回调地址：
         │  https://jupyter-callback.x.com/hub/oauth_callback?code=xxx
         │
5. JupyterHub 收到回调请求，通过 ALB 转发至本地服务
         │
6. JupyterHub 用 code 换取 token 并提取用户邮箱 (upn claim)
         │
7. 触发 normalize_username 处理：例如 zhang.san@x.com 规范化为 zhang-san
         │
8. 触发 pre_spawn_hook 钩子：
         │  - 自动创建 Linux 用户 zhang-san （如果一期已存在则直接复用）
         │  - 自动初始化工作目录并设为 700 权限
         │  - 自动为新用户构建 Conda 环境与 Kernel 注册
         │
9. 启动对应的 systemd 实例 jupyter-zhang-san.service，引导用户进入 JupyterLab
</code></pre></div></div>

<hr />

<h2 id="azure-ad-应用注册">Azure AD 应用注册</h2>

<p>在配置 JupyterHub 之前，需要登录 Azure Portal 注册企业应用。</p>

<h3 id="1-注册应用">1. 注册应用</h3>

<ol>
  <li>打开 <a href="https://portal.azure.com">Azure Portal</a>，依次进入 <strong>Microsoft Entra ID</strong> (原 Azure Active Directory) -&gt; <strong>应用注册</strong> -&gt; <strong>新注册</strong>。</li>
  <li>填写注册信息：
    <ul>
      <li><strong>名称</strong>：<code class="language-plaintext highlighter-rouge">JupyterHub</code></li>
      <li><strong>受支持的账户类型</strong>：根据租户范围选择（通常选择单租户）。</li>
      <li><strong>重定向 URI</strong>：平台选择 <code class="language-plaintext highlighter-rouge">Web</code>，值填写 <code class="language-plaintext highlighter-rouge">https://jupyter-callback.x.com/hub/oauth_callback</code>。</li>
    </ul>
  </li>
</ol>

<h3 id="2-获取必要凭证">2. 获取必要凭证</h3>

<p>在应用概述页和凭证页，记录以下关键信息用于 JupyterHub 配置：</p>

<ul>
  <li><strong>目录（租户）ID</strong> (<code class="language-plaintext highlighter-rouge">tenant_id</code>)：<code class="language-plaintext highlighter-rouge">xxxx</code></li>
  <li><strong>应用程序（客户端）ID</strong> (<code class="language-plaintext highlighter-rouge">client_id</code>)：<code class="language-plaintext highlighter-rouge">xxxx</code></li>
  <li><strong>客户端密码</strong> (<code class="language-plaintext highlighter-rouge">client_secret</code>)：在“证书和密码”页，新建客户端密码，并立即复制记录其 <strong>Value</strong>（注意不是密码 ID）。</li>
</ul>

<h3 id="3-配置-api-权限">3. 配置 API 权限</h3>

<p>在“API 权限”中，添加以下 <strong>Microsoft Graph</strong> 委托权限：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">openid</code></li>
  <li><code class="language-plaintext highlighter-rouge">email</code></li>
  <li><code class="language-plaintext highlighter-rouge">profile</code></li>
  <li><code class="language-plaintext highlighter-rouge">User.Read</code></li>
</ul>

<p><em>添加完成后，务必点击“代表 [您的组织] 授予管理员同意”。</em></p>

<hr />

<h2 id="环境依赖更新">环境依赖更新</h2>

<p>由于增加了 OAuth2 认证支持，需要在已有的 JupyterHub 虚拟环境中追加安装 <code class="language-plaintext highlighter-rouge">oauthenticator</code> 库：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>conda activate jupyterhub
pip <span class="nb">install </span>oauthenticator
</code></pre></div></div>

<hr />

<h2 id="完整-jupyterhub-配置">完整 JupyterHub 配置</h2>

<p>修改 <code class="language-plaintext highlighter-rouge">/etc/jupyterhub/jupyterhub_config.py</code>，配置 Azure AD 认证器，并实现自动创建 Linux 用户与环境的完整版钩子函数。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pwd</span>
<span class="kn">import</span> <span class="nn">grp</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">subprocess</span>

<span class="n">c</span> <span class="o">=</span> <span class="n">get_config</span><span class="p">()</span>  <span class="c1">#noqa
</span>
<span class="c1"># ═══════════════════════════════════════════════════════
# 全局变量
# ═══════════════════════════════════════════════════════
</span><span class="n">JUPYTER_USER_DIR</span> <span class="o">=</span> <span class="s">'/data/jupyter-users'</span>
<span class="n">JUPYTER_GROUP</span> <span class="o">=</span> <span class="s">'jupyterhub-users'</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# 网络配置
# ═══════════════════════════════════════════════════════
</span><span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">ip</span> <span class="o">=</span> <span class="s">'0.0.0.0'</span>
<span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">port</span> <span class="o">=</span> <span class="mi">8000</span>
<span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">base_url</span> <span class="o">=</span> <span class="s">'/'</span>

<span class="c1"># CRYPT_KEY：使用 openssl rand -hex 32 生成
</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'JUPYTERHUB_CRYPT_KEY'</span><span class="p">,</span> <span class="s">'xxxx'</span><span class="p">)</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# Azure AD OAuth 认证与用户名规范化
# ═══════════════════════════════════════════════════════
</span><span class="kn">from</span> <span class="nn">oauthenticator.azuread</span> <span class="kn">import</span> <span class="n">AzureAdOAuthenticator</span>

<span class="k">class</span> <span class="nc">NormalizedAzureAdOAuthenticator</span><span class="p">(</span><span class="n">AzureAdOAuthenticator</span><span class="p">):</span>
    <span class="s">"""
    覆写 normalize_username，确保 Azure AD 传回的邮箱格式能够无缝转换为合法的 Linux 用户名。
    转换示例：zhang.san@x.com → zhang-san
    """</span>
    <span class="k">def</span> <span class="nf">normalize_username</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">username</span><span class="p">):</span>
        <span class="c1"># 取 @ 前面的邮箱前缀
</span>        <span class="n">name</span> <span class="o">=</span> <span class="n">username</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">'@'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
        <span class="c1"># 将点、下划线、空格等统一转换为短横线，并转为小写
</span>        <span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'.'</span><span class="p">,</span> <span class="s">'-'</span><span class="p">).</span><span class="n">replace</span><span class="p">(</span><span class="s">'_'</span><span class="p">,</span> <span class="s">'-'</span><span class="p">).</span><span class="n">replace</span><span class="p">(</span><span class="s">' '</span><span class="p">,</span> <span class="s">'-'</span><span class="p">).</span><span class="n">lower</span><span class="p">()</span>
        <span class="c1"># 移除非法字符，限制仅能包含小写字母、数字和短横线
</span>        <span class="n">name</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s">'[^a-z0-9-]'</span><span class="p">,</span> <span class="s">''</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span>
        <span class="c1"># 截断长度（Linux 用户名限制最长 32 字符）
</span>        <span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">[:</span><span class="mi">32</span><span class="p">]</span>
        <span class="k">return</span> <span class="nb">super</span><span class="p">().</span><span class="n">normalize_username</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>

<span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">authenticator_class</span> <span class="o">=</span> <span class="n">NormalizedAzureAdOAuthenticator</span>

<span class="c1"># Azure AD 凭证配置
</span><span class="n">c</span><span class="p">.</span><span class="n">AzureAdOAuthenticator</span><span class="p">.</span><span class="n">tenant_id</span> <span class="o">=</span> <span class="s">'xxxx'</span>
<span class="n">c</span><span class="p">.</span><span class="n">AzureAdOAuthenticator</span><span class="p">.</span><span class="n">client_id</span> <span class="o">=</span> <span class="s">'xxxx'</span>
<span class="n">c</span><span class="p">.</span><span class="n">AzureAdOAuthenticator</span><span class="p">.</span><span class="n">client_secret</span> <span class="o">=</span> <span class="s">'xxxx'</span>
<span class="n">c</span><span class="p">.</span><span class="n">AzureAdOAuthenticator</span><span class="p">.</span><span class="n">oauth_callback_url</span> <span class="o">=</span> <span class="s">'https://jupyter-callback.x.com/hub/oauth_callback'</span>

<span class="c1"># 关键：指定 username_claim 为 upn（用户主体名），确保获取到合法的邮箱格式
</span><span class="n">c</span><span class="p">.</span><span class="n">AzureAdOAuthenticator</span><span class="p">.</span><span class="n">username_claim</span> <span class="o">=</span> <span class="s">'upn'</span>

<span class="c1"># 允许所有通过当前 Azure AD 租户认证的用户登录
</span><span class="n">c</span><span class="p">.</span><span class="n">AzureAdOAuthenticator</span><span class="p">.</span><span class="n">allow_all</span> <span class="o">=</span> <span class="bp">True</span>

<span class="c1"># 管理员用户配置（配置为规范化后的 Linux 用户名）
</span><span class="n">c</span><span class="p">.</span><span class="n">Authenticator</span><span class="p">.</span><span class="n">admin_users</span> <span class="o">=</span> <span class="p">{</span><span class="s">'admin'</span><span class="p">}</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# SystemdSpawner 配置
# ═══════════════════════════════════════════════════════
</span><span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">spawner_class</span> <span class="o">=</span> <span class="s">'systemdspawner.SystemdSpawner'</span>
<span class="n">c</span><span class="p">.</span><span class="n">Spawner</span><span class="p">.</span><span class="n">default_url</span> <span class="o">=</span> <span class="s">'/lab'</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">unit_name_template</span> <span class="o">=</span> <span class="s">'jupyter-{USERNAME}'</span>
<span class="n">c</span><span class="p">.</span><span class="n">Spawner</span><span class="p">.</span><span class="n">notebook_dir</span> <span class="o">=</span> <span class="n">JUPYTER_USER_DIR</span> <span class="o">+</span> <span class="s">'/{username}'</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">default_shell</span> <span class="o">=</span> <span class="s">'/bin/bash'</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">isolate_tmp</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">isolate_devices</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">disable_user_sudo</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="nb">slice</span> <span class="o">=</span> <span class="s">'jupyter-users.slice'</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# 自动化用户供给：pre_spawn_hook
# ═══════════════════════════════════════════════════════
</span><span class="n">CONDA_BIN</span> <span class="o">=</span> <span class="s">'/opt/miniconda3/bin/conda'</span>
<span class="n">USERADD_BIN</span> <span class="o">=</span> <span class="s">'/usr/sbin/useradd'</span>
<span class="n">GROUPADD_BIN</span> <span class="o">=</span> <span class="s">'/usr/sbin/groupadd'</span>

<span class="k">def</span> <span class="nf">ensure_system_user</span><span class="p">(</span><span class="n">spawner</span><span class="p">):</span>
    <span class="s">"""
    当用户首次通过 Azure AD 登录时，自动执行以下供给逻辑：
    1. 创建 JupyterHub 共享用户组
    2. 创建对应的 Linux 系统用户（禁止 SSH 登录但允许 Terminal 内执行 cron）
    3. 自动初始化 700 隔离工作目录
    4. 写入 Conda 环境变量至其 .bashrc
    5. 构建专属 Conda 隔离沙箱并注册 Jupyter 内核
    """</span>
    <span class="n">username</span> <span class="o">=</span> <span class="n">spawner</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">name</span>

    <span class="c1"># 严格校验：确保用户名必须符合规范化正则，拒绝非法输入直接写入系统命令
</span>    <span class="k">if</span> <span class="ow">not</span> <span class="n">re</span><span class="p">.</span><span class="n">fullmatch</span><span class="p">(</span><span class="sa">r</span><span class="s">'[a-z0-9-]{1,32}'</span><span class="p">,</span> <span class="n">username</span><span class="p">):</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span>
            <span class="sa">f</span><span class="s">'Refusing to spawn: username </span><span class="si">{</span><span class="n">username</span><span class="si">!r}</span><span class="s"> is not normalized. '</span>
            <span class="sa">f</span><span class="s">'请检查配置的 username_claim 或清理 jupyterhub.sqlite 中的脏数据。'</span>
        <span class="p">)</span>

    <span class="n">user_dir</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">JUPYTER_USER_DIR</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">'</span>
    <span class="n">conda_env_prefix</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">user_dir</span><span class="si">}</span><span class="s">/.conda/envs/</span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">'</span>

    <span class="c1"># 1. 确保 jupyterhub-users 组存在
</span>    <span class="k">try</span><span class="p">:</span>
        <span class="n">grp</span><span class="p">.</span><span class="n">getgrnam</span><span class="p">(</span><span class="n">JUPYTER_GROUP</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">KeyError</span><span class="p">:</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span><span class="n">GROUPADD_BIN</span><span class="p">,</span> <span class="n">JUPYTER_GROUP</span><span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="c1"># 2. 如果用户不存在则创建 Linux 用户
</span>    <span class="n">is_new_user</span> <span class="o">=</span> <span class="bp">False</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">pwd</span><span class="p">.</span><span class="n">getpwnam</span><span class="p">(</span><span class="n">username</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">KeyError</span><span class="p">:</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
            <span class="n">USERADD_BIN</span><span class="p">,</span>
            <span class="s">'-m'</span><span class="p">,</span>
            <span class="s">'-d'</span><span class="p">,</span> <span class="n">user_dir</span><span class="p">,</span>
            <span class="s">'-s'</span><span class="p">,</span> <span class="s">'/bin/bash'</span><span class="p">,</span>
            <span class="s">'-g'</span><span class="p">,</span> <span class="n">JUPYTER_GROUP</span><span class="p">,</span>
            <span class="s">'-G'</span><span class="p">,</span> <span class="s">'crontab'</span><span class="p">,</span>
            <span class="s">'--no-user-group'</span><span class="p">,</span>
            <span class="n">username</span>
        <span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="n">spawner</span><span class="p">.</span><span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'Created system user: </span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
        <span class="n">is_new_user</span> <span class="o">=</span> <span class="bp">True</span>

    <span class="c1"># 3. 确保目录存在并强制设定权限
</span>    <span class="n">os</span><span class="p">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">user_info</span> <span class="o">=</span> <span class="n">pwd</span><span class="p">.</span><span class="n">getpwnam</span><span class="p">(</span><span class="n">username</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">chmod</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="mo">0o700</span><span class="p">)</span>

    <span class="c1"># 4. 配置 .bashrc 中的 Conda PATH
</span>    <span class="k">if</span> <span class="n">is_new_user</span><span class="p">:</span>
        <span class="n">bashrc</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">user_dir</span><span class="si">}</span><span class="s">/.bashrc'</span>
        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">bashrc</span><span class="p">,</span> <span class="s">'a'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">f</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">export PATH="/opt/miniconda3/bin:$PATH"</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span>
        <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">bashrc</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>

    <span class="c1"># 5. 构建专属 Conda 沙箱及注册内核（仅在新环境未被初始化时执行）
</span>    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">conda_env_prefix</span><span class="p">):</span>
        <span class="n">spawner</span><span class="p">.</span><span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'Creating conda env: </span><span class="si">{</span><span class="n">conda_env_prefix</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
            <span class="n">CONDA_BIN</span><span class="p">,</span> <span class="s">'create'</span><span class="p">,</span> <span class="s">'-y'</span><span class="p">,</span>
            <span class="s">'-c'</span><span class="p">,</span> <span class="s">'conda-forge'</span><span class="p">,</span> <span class="s">'--override-channels'</span><span class="p">,</span>
            <span class="s">'--prefix'</span><span class="p">,</span> <span class="n">conda_env_prefix</span><span class="p">,</span>
            <span class="s">'python=3.12'</span>
        <span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        
        <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
            <span class="n">CONDA_BIN</span><span class="p">,</span> <span class="s">'run'</span><span class="p">,</span> <span class="s">'--prefix'</span><span class="p">,</span> <span class="n">conda_env_prefix</span><span class="p">,</span>
            <span class="s">'pip'</span><span class="p">,</span> <span class="s">'install'</span><span class="p">,</span> <span class="s">'ipykernel'</span>
        <span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

        <span class="n">kernel_prefix</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">user_dir</span><span class="si">}</span><span class="s">/.local'</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span>
            <span class="n">CONDA_BIN</span><span class="p">,</span> <span class="s">'run'</span><span class="p">,</span> <span class="s">'--prefix'</span><span class="p">,</span> <span class="n">conda_env_prefix</span><span class="p">,</span>
            <span class="s">'python'</span><span class="p">,</span> <span class="s">'-m'</span><span class="p">,</span> <span class="s">'ipykernel'</span><span class="p">,</span> <span class="s">'install'</span><span class="p">,</span>
            <span class="s">'--prefix'</span><span class="p">,</span> <span class="n">kernel_prefix</span><span class="p">,</span>
            <span class="s">'--name'</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span>
            <span class="s">'--display-name'</span><span class="p">,</span> <span class="sa">f</span><span class="s">'Python (</span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">)'</span>
        <span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

        <span class="c1"># 递归修正 conda 和 kernel specs 目录权限，以防以 root 身份创建时发生权限溢出
</span>        <span class="n">conda_dir</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">user_dir</span><span class="si">}</span><span class="s">/.conda'</span>
        <span class="k">for</span> <span class="n">root</span><span class="p">,</span> <span class="n">dirs</span><span class="p">,</span> <span class="n">files</span> <span class="ow">in</span> <span class="n">os</span><span class="p">.</span><span class="n">walk</span><span class="p">(</span><span class="n">conda_dir</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">dirs</span><span class="p">:</span>
                <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="n">d</span><span class="p">),</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">files</span><span class="p">:</span>
                <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="n">f</span><span class="p">),</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
        <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">conda_dir</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">root</span><span class="p">,</span> <span class="n">dirs</span><span class="p">,</span> <span class="n">files</span> <span class="ow">in</span> <span class="n">os</span><span class="p">.</span><span class="n">walk</span><span class="p">(</span><span class="n">kernel_prefix</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">dirs</span><span class="p">:</span>
                <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="n">d</span><span class="p">),</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">files</span><span class="p">:</span>
                <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="n">f</span><span class="p">),</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
        <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">kernel_prefix</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
        <span class="n">spawner</span><span class="p">.</span><span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'Conda env and kernel ready for user: </span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>

    <span class="n">spawner</span><span class="p">.</span><span class="n">log</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'User environment setup completed: </span><span class="si">{</span><span class="n">user_dir</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>

<span class="n">c</span><span class="p">.</span><span class="n">Spawner</span><span class="p">.</span><span class="n">pre_spawn_hook</span> <span class="o">=</span> <span class="n">ensure_system_user</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# 反向代理集成与 Cookie 域配置
# ═══════════════════════════════════════════════════════
</span><span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">tornado_settings</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'cookie_options'</span><span class="p">:</span> <span class="p">{</span><span class="s">'domain'</span><span class="p">:</span> <span class="s">'.x.com'</span><span class="p">},</span>
    <span class="s">'headers'</span><span class="p">:</span> <span class="p">{</span>
        <span class="s">'Content-Security-Policy'</span><span class="p">:</span> <span class="s">"frame-ancestors 'self'"</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="负载均衡器alb配置要点">负载均衡器（ALB）配置要点</h2>

<p>为了保证 OAuth2 的回调安全及 WebSocket 长连接的连贯性，ALB 侧的配置应遵循以下指标：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">配置项目</th>
      <th style="text-align: left">建议设定值</th>
      <th style="text-align: left">作用与说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>监听器</strong></td>
      <td style="text-align: left">HTTPS 443</td>
      <td style="text-align: left">绑定 <code class="language-plaintext highlighter-rouge">jupyter-callback.x.com</code> 的 SSL/TLS 证书</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>目标组</strong></td>
      <td style="text-align: left">HTTP 端口</td>
      <td style="text-align: left">转发至后端 JupyterHub 宿主机内网 IP 的 8000 端口</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>健康检查</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/hub/health</code></td>
      <td style="text-align: left">状态码预期 200，用以监控 JupyterHub 活动状态</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>会话粘性</strong></td>
      <td style="text-align: left">开启 (Sticky Session)</td>
      <td style="text-align: left">基于 Cookie 保证 Jupyter 容器的 WebSocket 连接不发生跨节点漂移</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>空闲超时</strong></td>
      <td style="text-align: left">3600 秒</td>
      <td style="text-align: left">避免由于长连接无心跳包被 ALB 提前掐断</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>转发头</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">X-Forwarded-Proto: https</code></td>
      <td style="text-align: left">告知 Tornado 后端当前请求为安全连接，防止重定向死循环</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="用户生命周期管理策略对照">用户生命周期管理策略对照</h2>

<p>在两种架构下，系统管理员对用户的生命周期管理操作如下：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">管理动作</th>
      <th style="text-align: left">第一期 (本地 PAM)</th>
      <th style="text-align: left">第二期 (Azure AD OAuth2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>新增用户</strong></td>
      <td style="text-align: left">手动执行 <code class="language-plaintext highlighter-rouge">useradd</code> -&gt; 手动设定密码 -&gt; 手动将用户名追加进 <code class="language-plaintext highlighter-rouge">allowed_users</code> 列表并重启服务</td>
      <td style="text-align: left">仅需在 Azure AD 侧为用户分配该企业应用访问权。用户首次登录时系统将全自动完成宿主机账户供给与 Conda 沙箱初始化。</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>禁用用户</strong></td>
      <td style="text-align: left">执行 <code class="language-plaintext highlighter-rouge">passwd -l [username]</code> 锁死本地账户 -&gt; 从 <code class="language-plaintext highlighter-rouge">allowed_users</code> 列表中移除</td>
      <td style="text-align: left">仅需在 Azure AD 侧禁用或删除该用户账号，用户再次访问将无法通过 OAuth2 验证。</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>删除用户数据</strong></td>
      <td style="text-align: left">手动执行 <code class="language-plaintext highlighter-rouge">userdel [username]</code> 并不加 <code class="language-plaintext highlighter-rouge">-r</code> 改为手动 <code class="language-plaintext highlighter-rouge">rm -rf /data/jupyter-users/[username]</code></td>
      <td style="text-align: left">手动执行 <code class="language-plaintext highlighter-rouge">userdel [username]</code> 并不加 <code class="language-plaintext highlighter-rouge">-r</code> 改为手动 <code class="language-plaintext highlighter-rouge">rm -rf /data/jupyter-users/[username]</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>管理员权限</strong></td>
      <td style="text-align: left">在 <code class="language-plaintext highlighter-rouge">admin_users</code> 集合中硬编码加入本地 Linux 用户名</td>
      <td style="text-align: left">在 <code class="language-plaintext highlighter-rouge">admin_users</code> 中填入规范化后的管理员邮箱前缀。登录后可直接通过 JupyterHub 后台的 Admin 控制面板进行界面化管理。</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="共享只读数据目录可选拓展">共享只读数据目录（可选拓展）</h2>

<p>如果团队内有公共数据集或共享代码包，需要提供给所有用户只读挂载：</p>

<ol>
  <li><strong>在宿主机创建共享目录并赋权</strong>：
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /data/jupyter-shared
<span class="nb">sudo chown </span>root:jupyterhub-users /data/jupyter-shared
<span class="nb">sudo chmod </span>750 /data/jupyter-shared
</code></pre></div>    </div>
  </li>
  <li><strong>在 <code class="language-plaintext highlighter-rouge">ensure_system_user</code> 函数末尾追加软链接建立逻辑</strong>：
    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 在 pre_spawn_hook 钩子函数的最后加入：
</span><span class="n">shared_link</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="s">'shared-data'</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">shared_link</span><span class="p">):</span>
    <span class="n">os</span><span class="p">.</span><span class="n">symlink</span><span class="p">(</span><span class="s">'/data/jupyter-shared'</span><span class="p">,</span> <span class="n">shared_link</span><span class="p">)</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>这样，每个用户在登录 JupyterLab 后，工作目录下都会自动出现一个名为 <code class="language-plaintext highlighter-rouge">shared-data</code> 的软链接，从而实现数据零拷贝的高效共享。</p>

<hr />]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="技术" /><category term="JupyterHub" /><category term="Azure AD" /><category term="OAuth2" /><category term="单点登录" /><summary type="html"><![CDATA[引言 在一期部署中，我们成功搭建了基于 SystemdSpawner 与本地 PAM 认证的多用户 JupyterHub。随着团队规模的扩大，手动管理 Linux 用户、维护密码不仅繁琐，而且无法满足企业对单点登录（SSO）与多因素认证（MFA）的安全合规要求。 本期我们将 JupyterHub 升级为 Azure AD（Microsoft Entra ID）OAuth2 认证。由于一期已经严格遵循了统一的用户名规范，本次升级无需迁移已有用户数据，可以实现无痛过渡。]]></summary></entry><entry><title type="html">JupyterHub 极简部署实践：基于 SystemdSpawner 与本地 PAM 认证 (一期)</title><link href="http://www.caritasem.com/2026/05/JupyterHub-step-by-step/" rel="alternate" type="text/html" title="JupyterHub 极简部署实践：基于 SystemdSpawner 与本地 PAM 认证 (一期)" /><published>2026-05-16T02:00:00+00:00</published><updated>2026-05-16T02:00:00+00:00</updated><id>http://www.caritasem.com/2026/05/JupyterHub-step-by-step</id><content type="html" xml:base="http://www.caritasem.com/2026/05/JupyterHub-step-by-step/"><![CDATA[<h2 id="为什么选择-jupyterhub--systemdspawner">为什么选择 JupyterHub + SystemdSpawner</h2>

<p>多用户 Jupyter 平台常见方案是基于 Kubernetes (Zero to JupyterHub) 或 Docker (DockerSpawner)。但对于中小团队或单机部署，Kubernetes 维护成本过高，DockerSpawner 存在数据权限映射和容器管理开销。</p>

<p>我们需要一个更轻量、贴合宿主机环境的方案。<strong>SystemdSpawner</strong> 是一个理想的选择：它能够利用 Linux 原生 systemd 服务的 cgroup 限制 CPU 和内存资源，且用户打开 Terminal 直接就是真实的宿主机 Shell，方便使用宿主机已安装的各种开发工具。这非常适合单机、多用户的高效共享开发服务器。</p>

<!--more-->

<h2 id="分期部署策略">分期部署策略</h2>

<p>为了平稳上线，部署工作分两期进行：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left"> </th>
      <th style="text-align: left">第一期</th>
      <th style="text-align: left">第二期</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>认证方式</strong></td>
      <td style="text-align: left">Linux PAM（系统用户密码）</td>
      <td style="text-align: left">Azure AD OAuth2</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>用户管理</strong></td>
      <td style="text-align: left">手动创建 Linux 用户</td>
      <td style="text-align: left">Azure AD 侧管理，首次登录自动创建</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>网络依赖</strong></td>
      <td style="text-align: left">纯内网，无需负载均衡(ALB)/公网域名</td>
      <td style="text-align: left">需要 ALB + 公网回调域名</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>外部依赖</strong></td>
      <td style="text-align: left">无</td>
      <td style="text-align: left">Azure AD 应用注册</td>
    </tr>
  </tbody>
</table>

<p><strong>无痛迁移的前提</strong>：第一期创建用户时，命名规则必须与第二期 <code class="language-plaintext highlighter-rouge">normalize_username</code> 函数输出一致（即邮箱 <code class="language-plaintext highlighter-rouge">@</code> 前部分，<code class="language-plaintext highlighter-rouge">.</code> 和 <code class="language-plaintext highlighter-rouge">_</code> 替换为 <code class="language-plaintext highlighter-rouge">-</code>，全小写）。例如 <code class="language-plaintext highlighter-rouge">zhang.san@x.com</code> → <code class="language-plaintext highlighter-rouge">zhang-san</code>。</p>

<p>这样在切换认证方式时，已有的 Linux 用户和 <code class="language-plaintext highlighter-rouge">/data/jupyter-users/{username}</code> 目录无需进行任何迁移，已有的用户数据可以完全保留。</p>

<hr />

<h2 id="一期架构概览">一期架构概览</h2>

<p>一期采用纯内网运行方式，架构如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>内网用户
  │
  ▼
http://jupyter.x.com  （内网 DNS，HTTP，80）
  │  内网 DNS 解析到内网 IP
  ▼
JupyterHub（0.0.0.0:8000）
  │
  ├─ 认证：PAM（Linux 系统用户密码）
  │
  └─ Spawner：SystemdSpawner
     └─ 每个用户 → 独立 systemd unit → cgroup 隔离
        └─ 工作目录 /data/jupyter-users/{username}（权限 700，互不可见）
</code></pre></div></div>

<hr />

<h2 id="命名规范">命名规范</h2>

<p>为确保第二期无缝切换，第一期创建用户时严格遵循以下规则：</p>

<ol>
  <li>取邮箱 <code class="language-plaintext highlighter-rouge">@</code> 前面的部分。</li>
  <li><code class="language-plaintext highlighter-rouge">.</code> 替换为 <code class="language-plaintext highlighter-rouge">-</code>。</li>
  <li><code class="language-plaintext highlighter-rouge">_</code> 替换为 <code class="language-plaintext highlighter-rouge">-</code>。</li>
  <li>全部小写。</li>
  <li>最长 32 字符。</li>
</ol>

<p>示例：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Azure AD 邮箱</th>
      <th style="text-align: left">Linux 用户名</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">zhang.san@x.com</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">zhang-san</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">li_si@x.com</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">li-si</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">admin@x.com</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">admin</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="系统依赖安装">系统依赖安装</h2>

<p>首先在 Ubuntu 服务器上安装基础依赖，并为 JupyterHub 构建独立的 Python 虚拟环境。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 基础依赖安装</span>
<span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> python3-pip python3-venv nodejs npm acl

<span class="c"># 创建虚拟环境（指定 conda-forge 源，避免 Anaconda 官方 ToS 拦截）</span>
conda create <span class="nt">-y</span> <span class="nt">-c</span> conda-forge <span class="nt">--override-channels</span> <span class="nt">--prefix</span> /data/condaenv/jupyterhub <span class="nv">python</span><span class="o">=</span>3.13
conda activate jupyterhub 

<span class="c"># 安装 JupyterHub 和 SystemdSpawner</span>
pip <span class="nb">install </span>jupyterhub jupyterlab jupyterhub-systemdspawner

<span class="c"># 安装代理组件</span>
<span class="nb">sudo </span>npm <span class="nb">install</span> <span class="nt">-g</span> configurable-http-proxy

<span class="c"># 创建数据根目录</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /data/jupyter-users
<span class="nb">sudo chmod </span>755 /data/jupyter-users
</code></pre></div></div>

<hr />

<h2 id="第一期配置文件">第一期配置文件</h2>

<p>创建配置文件目录并写入 <code class="language-plaintext highlighter-rouge">/etc/jupyterhub/jupyterhub_config.py</code>。</p>

<p>一期配置文件中，主要使用本地 <code class="language-plaintext highlighter-rouge">PAMAuthenticator</code>，并在 <code class="language-plaintext highlighter-rouge">pre_spawn_hook</code> 中修正手动创建的用户的目录权限。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pwd</span>
<span class="kn">import</span> <span class="nn">grp</span>
<span class="kn">import</span> <span class="nn">subprocess</span>

<span class="n">c</span> <span class="o">=</span> <span class="n">get_config</span><span class="p">()</span>  <span class="c1">#noqa
</span>
<span class="c1"># ═══════════════════════════════════════════════════════
# 全局变量
# ═══════════════════════════════════════════════════════
</span><span class="n">JUPYTER_USER_DIR</span> <span class="o">=</span> <span class="s">'/data/jupyter-users'</span>
<span class="n">JUPYTER_GROUP</span> <span class="o">=</span> <span class="s">'jupyterhub-users'</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# 网络配置
# ═══════════════════════════════════════════════════════
</span><span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">ip</span> <span class="o">=</span> <span class="s">'0.0.0.0'</span>
<span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">port</span> <span class="o">=</span> <span class="mi">8000</span>
<span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">base_url</span> <span class="o">=</span> <span class="s">'/'</span>

<span class="c1"># CRYPT_KEY：使用 openssl rand -hex 32 生成
</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'JUPYTERHUB_CRYPT_KEY'</span><span class="p">,</span> <span class="s">'xxxx'</span><span class="p">)</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# 第一期：PAM 本地认证
# ═══════════════════════════════════════════════════════
</span><span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">authenticator_class</span> <span class="o">=</span> <span class="s">'jupyterhub.auth.PAMAuthenticator'</span>

<span class="c1"># 管理员与允许登录的用户列表
</span><span class="n">c</span><span class="p">.</span><span class="n">Authenticator</span><span class="p">.</span><span class="n">admin_users</span> <span class="o">=</span> <span class="p">{</span><span class="s">'admin'</span><span class="p">}</span>
<span class="n">c</span><span class="p">.</span><span class="n">Authenticator</span><span class="p">.</span><span class="n">allowed_users</span> <span class="o">=</span> <span class="p">{</span><span class="s">'admin'</span><span class="p">}</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# SystemdSpawner 配置
# ═══════════════════════════════════════════════════════
</span><span class="n">c</span><span class="p">.</span><span class="n">JupyterHub</span><span class="p">.</span><span class="n">spawner_class</span> <span class="o">=</span> <span class="s">'systemdspawner.SystemdSpawner'</span>
<span class="n">c</span><span class="p">.</span><span class="n">Spawner</span><span class="p">.</span><span class="n">default_url</span> <span class="o">=</span> <span class="s">'/lab'</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">unit_name_template</span> <span class="o">=</span> <span class="s">'jupyter-{USERNAME}'</span>
<span class="n">c</span><span class="p">.</span><span class="n">Spawner</span><span class="p">.</span><span class="n">notebook_dir</span> <span class="o">=</span> <span class="n">JUPYTER_USER_DIR</span> <span class="o">+</span> <span class="s">'/{username}'</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">default_shell</span> <span class="o">=</span> <span class="s">'/bin/bash'</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">isolate_tmp</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">isolate_devices</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">c</span><span class="p">.</span><span class="n">SystemdSpawner</span><span class="p">.</span><span class="n">disable_user_sudo</span> <span class="o">=</span> <span class="bp">True</span>

<span class="c1"># ═══════════════════════════════════════════════════════
# pre_spawn_hook（确保目录存在并赋权，一期用户已手动创建）
# ═══════════════════════════════════════════════════════
</span><span class="k">def</span> <span class="nf">ensure_system_user</span><span class="p">(</span><span class="n">spawner</span><span class="p">):</span>
    <span class="n">username</span> <span class="o">=</span> <span class="n">spawner</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">name</span>
    <span class="n">user_dir</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">JUPYTER_USER_DIR</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">'</span>
    <span class="n">os</span><span class="p">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">user_info</span> <span class="o">=</span> <span class="n">pwd</span><span class="p">.</span><span class="n">getpwnam</span><span class="p">(</span><span class="n">username</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">chown</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_uid</span><span class="p">,</span> <span class="n">user_info</span><span class="p">.</span><span class="n">pw_gid</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">chmod</span><span class="p">(</span><span class="n">user_dir</span><span class="p">,</span> <span class="mo">0o700</span><span class="p">)</span>

<span class="n">c</span><span class="p">.</span><span class="n">Spawner</span><span class="p">.</span><span class="n">pre_spawn_hook</span> <span class="o">=</span> <span class="n">ensure_system_user</span>
</code></pre></div></div>

<hr />

<h2 id="用户管理与环境初始化">用户管理与环境初始化</h2>

<p>一期需要手动在 Linux 宿主机上创建用户，并限制其登录权限，同时为其初始化专属的 Conda 环境和内核。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 创建用户组（一次性）</span>
<span class="nb">sudo </span>groupadd jupyterhub-users

<span class="c"># 禁止 jupyterhub-users 组通过 SSH 登录</span>
<span class="nb">echo</span> <span class="s1">'DenyGroups jupyterhub-users'</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/ssh/sshd_config
<span class="nb">sudo </span>systemctl reload ssh

<span class="c"># 新增用户（用户名按命名规范命名）</span>
<span class="nv">USERNAME</span><span class="o">=</span>admin
<span class="c"># 使用 -m 选项自动创建目录并拷贝 /etc/skel 默认配置</span>
<span class="nb">sudo </span>useradd <span class="nt">-m</span> <span class="nt">-d</span> /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="nt">-s</span> /bin/bash <span class="nt">-g</span> jupyterhub-users <span class="nt">-G</span> crontab <span class="nt">--no-user-group</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>
<span class="nb">sudo </span>passwd <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>

<span class="c"># 修正权限，保证目录仅限用户本人访问</span>
<span class="nb">sudo chmod </span>700 /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>

<span class="c"># 写入 conda PATH，使用户 Terminal 能直接使用 conda 命令</span>
<span class="nb">echo</span> <span class="s1">'export PATH="/opt/miniconda3/bin:$PATH"'</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>/.bashrc

<span class="c"># 创建与用户名同名的 conda 环境，并注册为 Jupyter kernel</span>
<span class="nb">sudo</span> <span class="nt">-u</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> /opt/miniconda3/bin/conda create <span class="nt">-y</span> <span class="nt">-c</span> conda-forge <span class="nt">--override-channels</span> <span class="nt">--prefix</span> /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>/.conda/envs/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="nv">python</span><span class="o">=</span>3.12
<span class="nb">sudo</span> <span class="nt">-u</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> /opt/miniconda3/bin/conda run <span class="nt">--prefix</span> /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>/.conda/envs/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="se">\</span>
    pip <span class="nb">install </span>ipykernel
<span class="nb">sudo</span> <span class="nt">-u</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> /opt/miniconda3/bin/conda run <span class="nt">--prefix</span> /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>/.conda/envs/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="se">\</span>
    python <span class="nt">-m</span> ipykernel <span class="nb">install</span> <span class="nt">--user</span> <span class="nt">--name</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="nt">--display-name</span> <span class="s2">"Python (</span><span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span><span class="s2">)"</span>

<span class="c"># 修正 .conda 目录的所属用户和组</span>
<span class="nb">sudo chown</span> <span class="nt">-R</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>:jupyterhub-users /data/jupyter-users/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>/.conda
</code></pre></div></div>

<p><em>注意：每当手动新增用户后，必须在 <code class="language-plaintext highlighter-rouge">jupyterhub_config.py</code> 的 <code class="language-plaintext highlighter-rouge">allowed_users</code> 中添加对应用户名，然后重启 JupyterHub。</em></p>

<hr />

<h2 id="systemd-服务与资源限制">Systemd 服务与资源限制</h2>

<h3 id="51-jupyterhub-systemd-配置">5.1 JupyterHub Systemd 配置</h3>

<p>创建 <code class="language-plaintext highlighter-rouge">/etc/systemd/system/jupyterhub.service</code>：</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Unit]</span>
<span class="py">Description</span><span class="p">=</span><span class="s">JupyterHub</span>
<span class="py">After</span><span class="p">=</span><span class="s">network.target</span>

<span class="nn">[Service]</span>
<span class="c"># SystemdSpawner 需要 root 权限来管理用户级 systemd unit
</span><span class="py">User</span><span class="p">=</span><span class="s">root</span>
<span class="c"># 必须包含 /usr/sbin，确保 subprocess 能够找到 useradd / groupadd 命令
</span><span class="py">Environment</span><span class="p">=</span><span class="s">PATH=/data/condaenv/jupyterhub/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/data/condaenv/jupyterhub/bin/jupyterhub -f /etc/jupyterhub/jupyterhub_config.py</span>
<span class="py">WorkingDirectory</span><span class="p">=</span><span class="s">/etc/jupyterhub</span>
<span class="py">Restart</span><span class="p">=</span><span class="s">on-failure</span>
<span class="py">RestartSec</span><span class="p">=</span><span class="s">10</span>

<span class="nn">[Install]</span>
<span class="py">WantedBy</span><span class="p">=</span><span class="s">multi-user.target</span>
</code></pre></div></div>

<p>启动并使能服务：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl daemon-reload
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> jupyterhub
</code></pre></div></div>

<h3 id="52-资源限制systemd-slice">5.2 资源限制（systemd slice）</h3>

<p>为防止单用户资源超载导致整机崩溃，需要将所有用户进程归入 <code class="language-plaintext highlighter-rouge">jupyter-users.slice</code> 中进行统一限制。</p>

<p>创建 <code class="language-plaintext highlighter-rouge">/etc/systemd/system/jupyter-users.slice</code>：</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Slice]</span>
<span class="py">Description</span><span class="p">=</span><span class="s">Resource limits for all JupyterHub user notebooks</span>
<span class="c"># 示例：以 8核 32G 机器为例，限制用户总体 CPU 792%，内存最大 31G，达到 26G 开始触发软限制回收
</span><span class="py">CPUQuota</span><span class="p">=</span><span class="s">792%</span>
<span class="py">MemoryMax</span><span class="p">=</span><span class="s">31G</span>
<span class="py">MemoryHigh</span><span class="p">=</span><span class="s">26G</span>
</code></pre></div></div>

<p>为了适应机器规格的自动调整，推荐使用动态配置脚本 <code class="language-plaintext highlighter-rouge">/usr/local/bin/set-jupyter-limits.sh</code>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">set</span> <span class="nt">-e</span>

<span class="nv">CPU_CORES</span><span class="o">=</span><span class="si">$(</span><span class="nb">nproc</span><span class="si">)</span>
<span class="nv">MEM_TOTAL</span><span class="o">=</span><span class="si">$(</span><span class="nb">awk</span> <span class="s1">'/MemTotal/ {printf "%.0f", $2/1024/1024}'</span> /proc/meminfo<span class="si">)</span>

<span class="c"># 留出 1% 给系统和 JupyterHub 自身</span>
<span class="nv">CPU_LIMIT</span><span class="o">=</span><span class="k">$((</span>CPU_CORES <span class="o">*</span> <span class="m">99</span><span class="k">))</span>
<span class="nv">MEM_LIMIT</span><span class="o">=</span><span class="k">$((</span>MEM_TOTAL <span class="o">*</span> <span class="m">99</span> <span class="o">/</span> <span class="m">100</span><span class="k">))</span>
<span class="nv">MEM_HIGH</span><span class="o">=</span><span class="k">$((</span>MEM_LIMIT <span class="o">*</span> <span class="m">85</span> <span class="o">/</span> <span class="m">100</span><span class="k">))</span>

<span class="nb">mkdir</span> <span class="nt">-p</span> /etc/systemd/system/jupyter-users.slice.d/
<span class="nb">cat</span> <span class="o">&gt;</span> /etc/systemd/system/jupyter-users.slice.d/resource-limit.conf <span class="o">&lt;&lt;</span> <span class="no">CONF</span><span class="sh">
[Slice]
CPUQuota=</span><span class="k">${</span><span class="nv">CPU_LIMIT</span><span class="k">}</span><span class="sh">%
MemoryMax=</span><span class="k">${</span><span class="nv">MEM_LIMIT</span><span class="k">}</span><span class="sh">G
MemoryHigh=</span><span class="k">${</span><span class="nv">MEM_HIGH</span><span class="k">}</span><span class="sh">G
</span><span class="no">CONF

</span>systemctl daemon-reload
<span class="nb">echo</span> <span class="s2">"Jupyter user limits set: CPU=</span><span class="k">${</span><span class="nv">CPU_LIMIT</span><span class="k">}</span><span class="s2">%, MemMax=</span><span class="k">${</span><span class="nv">MEM_LIMIT</span><span class="k">}</span><span class="s2">G, MemHigh=</span><span class="k">${</span><span class="nv">MEM_HIGH</span><span class="k">}</span><span class="s2">G"</span>
</code></pre></div></div>

<p>赋予执行权限并创建开机自启服务：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chmod</span> +x /usr/local/bin/set-jupyter-limits.sh
<span class="nb">sudo</span> /usr/local/bin/set-jupyter-limits.sh

<span class="c"># 创建开机自启动 oneshot 服务</span>
<span class="nb">sudo tee</span> /etc/systemd/system/jupyter-resource-limits.service <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
[Unit]
Description=Dynamic Jupyter user resource limits
Before=jupyterhub.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/set-jupyter-limits.sh

[Install]
WantedBy=multi-user.target
</span><span class="no">EOF

</span><span class="nb">sudo </span>systemctl <span class="nb">enable </span>jupyter-resource-limits.service
</code></pre></div></div>

<hr />

<h2 id="数据隔离验证">数据隔离验证</h2>

<p>部署完毕后，登录不同的账户以验证多用户隔离效果：</p>

<ol>
  <li><strong>检查目录权限</strong>：
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> <span class="nt">-la</span> /data/jupyter-users/
<span class="c"># 应显示：</span>
<span class="c"># drwx------  zhang-san jupyterhub-users  zhang-san/</span>
<span class="c"># drwx------  li-si     jupyterhub-users  li-si/</span>
</code></pre></div>    </div>
  </li>
  <li><strong>跨目录访问拦截</strong>：<br />
使用 <code class="language-plaintext highlighter-rouge">zhang-san</code> 身份尝试访问 <code class="language-plaintext highlighter-rouge">li-si</code> 的目录应被系统拦截：
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-u</span> zhang-san <span class="nb">ls</span> /data/jupyter-users/li-si/
<span class="c"># 预期输出：ls: cannot open directory '/data/jupyter-users/li-si/': Permission denied</span>
</code></pre></div>    </div>
  </li>
  <li><strong>Jupyter Terminal 内测试</strong>：<br />
在 JupyterLab 中打开 Terminal 执行 <code class="language-plaintext highlighter-rouge">ls /data/jupyter-users/</code>，能够看到目录名称，但尝试读取任何其他用户文件都将返回 <code class="language-plaintext highlighter-rouge">Permission denied</code>。</li>
</ol>

<hr />]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="技术" /><category term="JupyterHub" /><category term="SystemdSpawner" /><category term="运维" /><category term="Linux" /><summary type="html"><![CDATA[为什么选择 JupyterHub + SystemdSpawner 多用户 Jupyter 平台常见方案是基于 Kubernetes (Zero to JupyterHub) 或 Docker (DockerSpawner)。但对于中小团队或单机部署，Kubernetes 维护成本过高，DockerSpawner 存在数据权限映射和容器管理开销。 我们需要一个更轻量、贴合宿主机环境的方案。SystemdSpawner 是一个理想的选择：它能够利用 Linux 原生 systemd 服务的 cgroup 限制 CPU 和内存资源，且用户打开 Terminal 直接就是真实的宿主机 Shell，方便使用宿主机已安装的各种开发工具。这非常适合单机、多用户的高效共享开发服务器。]]></summary></entry><entry><title type="html">如何使用 NetBird 快速构建企业级安全内网</title><link href="http://www.caritasem.com/2026/04/how-to-use-netbird-to-build-enterprise-network/" rel="alternate" type="text/html" title="如何使用 NetBird 快速构建企业级安全内网" /><published>2026-04-25T15:30:00+00:00</published><updated>2026-04-25T15:30:00+00:00</updated><id>http://www.caritasem.com/2026/04/how-to-use-netbird-to-build-enterprise-network</id><content type="html" xml:base="http://www.caritasem.com/2026/04/how-to-use-netbird-to-build-enterprise-network/"><![CDATA[<h2 id="为什么选择-netbird">为什么选择 NetBird</h2>

<p>随着企业业务的分布式发展和远程办公的普及，构建一个安全、高效且易于管理的内网变得至关重要。传统的 VPN 解决方案（如 OpenVPN、IPsec）配置繁琐，对客户端支持有限，且由于中心化架构容易产生网络瓶颈。</p>

<p>我们需要一个现代化的替代方案，它应该具备以下特质：部署简单、配置下发自动化、基于点对点直连（降低延迟），且拥有直观的管理面板。这就是我们选择 <strong>NetBird</strong> 的原因。它不仅满足上述所有需求，还完美地平衡了企业级安全需求与极简的运维体验。</p>

<!--more-->

<h2 id="netbird-介绍">NetBird 介绍</h2>

<p>NetBird 是一个开源的现代零信任私有网络管理平台。它基于 WireGuard 协议构建，通过创建点对点（P2P）Mesh 网络，让位于不同地理位置、不同网络环境中的设备能够像身处同一个局域网内一样安全通信。</p>

<p>它包含四个核心组件：</p>
<ul>
  <li><strong>Management Service (管理服务)</strong>：集中控制平面，负责设备注册、认证、IP分配和访问控制规则的分发。</li>
  <li><strong>Client Application (客户端)</strong>：安装在各终端节点上的代理程序，负责生成密钥并与其它节点建立 WireGuard 隧道。</li>
  <li><strong>Signal Service (信令服务)</strong>：帮助处于 NAT 后的节点互相发现并协商直连通道（类似 WebRTC）。</li>
  <li><strong>Relay Service (中继服务)</strong>：当节点间无法直接打洞穿透 NAT 时，作为备用方案进行流量转发（基于 WebSocket 或 TURN）。</li>
</ul>

<h2 id="netbird-网络架构">NetBird 网络架构</h2>

<p>NetBird 的架构设计精简而高效。一旦设备通过管理节点认证并获取了网络状态信息，设备之间就会尽可能建立点对点的 WireGuard 加密隧道，流量无需经过中心节点转发，极大提升了网络性能。</p>

<p><img src="https://docs.netbird.io/docs-static/img/about-netbird/high-level-dia.png" alt="NetBird 网络架构" /></p>

<h2 id="netbird-特点">NetBird 特点</h2>

<h3 id="安全性基于-wireguard">安全性，基于 WireGuard</h3>
<p>NetBird 底层使用 WireGuard 协议进行数据传输。WireGuard 相比传统协议代码更少、更现代，且原生提供最先进的密码学加密机制。客户端在本地生成私钥，仅向管理服务上传公钥，确保任何第三方（甚至包括管理服务本身）都无法解密点对点传输的数据。</p>

<h3 id="易用性贴心的-web-管理后台">易用性，贴心的 Web 管理后台</h3>
<p>与传统基于命令行或复杂配置文件的方案不同，NetBird 提供了一个现代、直观的 Web 控制台（Control Center）。管理员可以在可视化的界面中管理所有节点（Peers）、配置访问控制策略（ACL）、管理团队成员并配置 SSO 单点登录，极大降低了运维复杂度。</p>

<h3 id="支持路由指定和下发无须客户端配置">支持路由指定和下发，无须客户端配置</h3>
<p>NetBird 允许在 Web 后台配置网络路由（Network Routes）。你可以指定某一个节点作为特定子网的出口网关，配置完成后，这些路由规则会自动下发到所有相关客户端。客户端无需进行任何手动干预或命令行操作，即可无缝访问指定的企业内部资源。</p>

<h3 id="图形化客户端对非技术人员友好">图形化客户端，对非技术人员友好</h3>
<p>除了后端的易用性，NetBird 还为各大主流操作系统（Windows, macOS, 移动端等）提供了界面简洁的图形化客户端软件。对于团队中不具备网络配置经验的非技术人员（如设计、财务、产品经理），只需登录账号即可一键接入内网，完全没有使用门槛。</p>

<h2 id="对比其他解决方案">对比其他解决方案</h2>

<p>在做技术选型时，我们将 NetBird 与其他主流组网方案进行了对比：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">特性 / 方案</th>
      <th style="text-align: left">NetBird</th>
      <th style="text-align: left">Tailscale</th>
      <th style="text-align: left">OpenVPN / IPsec</th>
      <th style="text-align: left">ZeroTier</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>底层协议</strong></td>
      <td style="text-align: left">WireGuard</td>
      <td style="text-align: left">WireGuard</td>
      <td style="text-align: left">OpenVPN / IPsec</td>
      <td style="text-align: left">专有协议</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>网络拓扑</strong></td>
      <td style="text-align: left">P2P Mesh</td>
      <td style="text-align: left">P2P Mesh</td>
      <td style="text-align: left">星型 (中心化)</td>
      <td style="text-align: left">P2P Mesh</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>开源程度</strong></td>
      <td style="text-align: left">完全开源 (含控制端)</td>
      <td style="text-align: left">仅客户端开源</td>
      <td style="text-align: left">开源</td>
      <td style="text-align: left">核心开源</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>自建成本</strong></td>
      <td style="text-align: left">极低 (一键脚本)</td>
      <td style="text-align: left">较高 (需依赖 Headscale)</td>
      <td style="text-align: left">中等 (配置复杂)</td>
      <td style="text-align: left">中等 (管理面板复杂)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Web 管理后台</strong></td>
      <td style="text-align: left">自带，体验极佳</td>
      <td style="text-align: left">官方提供 (免费额度有限)</td>
      <td style="text-align: left">需第三方或手写配置</td>
      <td style="text-align: left">官方提供</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>路由下发</strong></td>
      <td style="text-align: left">支持，全自动</td>
      <td style="text-align: left">支持</td>
      <td style="text-align: left">需手动配置推流</td>
      <td style="text-align: left">支持</td>
    </tr>
  </tbody>
</table>

<p><em>总结：对于需要完全开源、数据私有化部署且追求极致易用性的企业来说，NetBird 是目前的最佳选择。</em></p>

<h2 id="netbird-服务端安装过程">NetBird 服务端安装过程</h2>

<p>NetBird 服务端（即管理后台、信令服务等控制面组件）的自托管部署非常迅速，官方提供了一键安装脚本。</p>

<p><strong>基础设施要求：</strong></p>
<ul>
  <li>一台具有公网 IP 的 Linux 服务器（至少 1核 2GB 内存）。</li>
  <li>服务器放行 TCP 80, 443 端口，以及 UDP 3478 端口。</li>
  <li>准备一个解析到该服务器 IP 的公网域名（如 <code class="language-plaintext highlighter-rouge">netbird.example.com</code> 和 <code class="language-plaintext highlighter-rouge">*.netbird.example.com</code>）。</li>
  <li>已安装 Docker 及其 Compose 插件，以及 <code class="language-plaintext highlighter-rouge">jq</code> 和 <code class="language-plaintext highlighter-rouge">curl</code> 工具。</li>
</ul>

<p><strong>详细安装步骤：</strong></p>

<p><strong>第一步：域名解析与环境准备</strong><br />
在开始前，请登录你的域名服务商控制台，添加两条 A 记录指向你的服务器 IP：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">@</code> 或 <code class="language-plaintext highlighter-rouge">netbird</code> 指向服务器 IP（用于管理后台）。</li>
  <li><code class="language-plaintext highlighter-rouge">*</code> 或 <code class="language-plaintext highlighter-rouge">*.netbird</code> 指向服务器 IP（如果开启 Proxy 或需要泛域名证书）。<br />
确保服务器已安装 Docker (含 docker-compose) 以及 <code class="language-plaintext highlighter-rouge">curl</code>、<code class="language-plaintext highlighter-rouge">jq</code>。</li>
</ul>

<p><strong>第二步：执行安装脚本</strong><br />
在服务器终端执行官方的快速安装脚本：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash
</code></pre></div></div>

<p><strong>第三步：配置反向代理（自动化处理）</strong><br />
脚本运行后，会弹出一个交互式提示，询问你使用哪种反向代理：</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Which reverse proxy will you use?
[0] Traefik (recommended)
[1] Existing Traefik ...
</code></pre></div></div>
<p>直接按回车选择默认的 <strong><code class="language-plaintext highlighter-rouge">[0] Traefik</code></strong>。它会在 Docker 里为你搞定一切，并自动通过 Let’s Encrypt 申请和续期 HTTPS 证书。</p>

<p><strong>第四步：配置安全与代理组件</strong><br />
紧接着，脚本会询问是否开启 <strong>NetBird Proxy</strong> 服务（允许你把内网服务安全地暴露到公网），输入 <code class="language-plaintext highlighter-rouge">y</code> 开启。<br />
如果开启了 Proxy，还会询问是否启用 <strong>CrowdSec</strong>（自动拦截恶意 IP 和网络攻击），建议一并开启以提升安全性。</p>

<p><strong>第五步：初始化系统与账号</strong><br />
安装完成并且所有 Docker 容器成功启动后，打开浏览器访问你的域名：<code class="language-plaintext highlighter-rouge">https://netbird.example.com</code>。<br />
系统检测到是初次部署，会自动跳转到 <code class="language-plaintext highlighter-rouge">/setup</code> 页面。在这里：</p>
<ol>
  <li>输入你的邮箱和名字。</li>
  <li>设置你的管理员密码。</li>
  <li>点击创建账号。</li>
</ol>

<p>完成账号创建后，你就会进入全新的 NetBird Web 控制台。至此，管理端搭建完毕，你可以下载客户端软件，开始添加你的第一台企业内网设备了！</p>

<p><strong>第六步：进程常驻与开机自启</strong><br />
对于 NetBird 服务端来说，你不需要像配置传统软件那样手动编写 <code class="language-plaintext highlighter-rouge">/etc/systemd/system/netbird.service</code>。官方安装脚本生成的 Docker Compose 配置中已经自带了容器保活策略。你只需要确保服务器本身的 Docker 守护进程设置为开机启动即可：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable </span>docker
<span class="nb">sudo </span>systemctl start docker
</code></pre></div></div>
<p>只要 Docker 服务在运行，NetBird 的所有服务端容器（包括数据库、面板、中继组件等）就会自动常驻后台并跟随服务器开机自启，省去了大量的手动运维工作。</p>

<hr />

<h2 id="参考文献">参考文献</h2>

<ul>
  <li><a href="https://docs.netbird.io/selfhosted/selfhosted-quickstart">NetBird Self-Hosting Quickstart</a></li>
  <li><a href="https://docs.netbird.io/about-netbird/how-netbird-works">How NetBird Works</a></li>
</ul>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="技术" /><category term="NetBird" /><category term="VPN" /><category term="WireGuard" /><category term="内网安全" /><summary type="html"><![CDATA[为什么选择 NetBird 随着企业业务的分布式发展和远程办公的普及，构建一个安全、高效且易于管理的内网变得至关重要。传统的 VPN 解决方案（如 OpenVPN、IPsec）配置繁琐，对客户端支持有限，且由于中心化架构容易产生网络瓶颈。 我们需要一个现代化的替代方案，它应该具备以下特质：部署简单、配置下发自动化、基于点对点直连（降低延迟），且拥有直观的管理面板。这就是我们选择 NetBird 的原因。它不仅满足上述所有需求，还完美地平衡了企业级安全需求与极简的运维体验。]]></summary></entry><entry><title type="html">Rete 算法快速入门（Python 演示版）</title><link href="http://www.caritasem.com/2026/04/rete-algorithm-explained/" rel="alternate" type="text/html" title="Rete 算法快速入门（Python 演示版）" /><published>2026-04-20T02:00:00+00:00</published><updated>2026-04-20T02:00:00+00:00</updated><id>http://www.caritasem.com/2026/04/rete-algorithm-explained</id><content type="html" xml:base="http://www.caritasem.com/2026/04/rete-algorithm-explained/"><![CDATA[<p>本文对 Rete 算法的执行架构、网络拓扑、编译与匹配过程进行说明，并附带 Python 代码模拟。</p>

<!--more-->

<h2 id="1-规则引擎执行架构match-select-act">1. 规则引擎执行架构（Match-Select-Act）</h2>

<p>首先要理解 <strong>事实 (Fact)</strong> 与 <strong>规则 (Rule)</strong> 是如何交汇的。</p>

<pre><code class="language-mermaid">graph LR
    subgraph 业务端
        Fact[Facts 事实对象]
        Rules[Rules 规则描述]
    end
    
    subgraph 规则引擎运行时核心
        WM[(工作内存&lt;br/&gt;Working Memory)]
        PM[(规则内存&lt;br/&gt;Production Memory)]
        Matcher{Rete 模式匹配器&lt;br/&gt;Pattern Matcher}
        Agenda[议程控制&lt;br/&gt;Agenda]
    end
    
    Fact --&gt;|1. Insert 断言插入| WM
    Rules --&gt;|2. 加载编译入库| PM
    
    WM --&gt;|事实进入| Matcher
    PM --&gt;|构建网络| Matcher
    
    Matcher --&gt;|3. 条件匹配成功的激活项| Agenda
    Agenda --&gt;|4. 按优先级解决冲突并执行| Action[触发操作逻辑 / RHS]
    Action -.-&gt;|5. 可能会产生或修改事实| WM
</code></pre>

<p>执行分三个阶段：</p>
<ul>
  <li><strong>匹配 (Match)</strong>：Fact 插入工作内存，通过 Rete 网络与规则条件建立绑定。</li>
  <li><strong>选择 (Select)</strong>：匹配成功的结果放入 Agenda，根据优先级决定执行顺序。</li>
  <li><strong>执行 (Act)</strong>：触发动作，可能引起新的事实变更，从而触发新一轮匹配。</li>
</ul>

<h2 id="2-rete-网络结构">2. Rete 网络结构</h2>

<p>当规则包含多个条件时，Rete 会构建多层网络，分为两部分：</p>

<ul>
  <li><strong>Alpha Network</strong>：对单个事实做属性过滤</li>
  <li><strong>Beta Network</strong>：将多个 Alpha 节点的结果做 Join 连接</li>
</ul>

<pre><code class="language-mermaid">graph TD
    subgraph AlphaLayer [Alpha层 单条件过滤]
        Root[RootNode&lt;br/&gt;数据总入口]
        OTN[ObjectType Node&lt;br/&gt;对象类型分发]
        
        Root --&gt; OTN
        OTN --&gt; Alpha1[Alpha 1: 地区=北京]
        OTN --&gt; Alpha2[Alpha 2: 车龄&lt;10年]
        OTN --&gt; Alpha3[Alpha 3: 车价&gt;1万元]
    end

    subgraph BetaLayer [Beta层 条件组合 / Join]
        Beta1((Beta Node 1&lt;br/&gt;地区 + 车龄))
        Alpha1 --&gt;|左输入| Beta1
        Alpha2 --&gt;|右输入| Beta1

        Beta2((Beta Node 2&lt;br/&gt;前置结果 + 车价))
        Beta1 --&gt;|左输入| Beta2
        Alpha3 --&gt;|右输入| Beta2
    end

    subgraph ActionLayer [执行区]
        Terminal[Terminal Node&lt;br/&gt;条件满足,执行规则]
        Beta2 --&gt; Terminal
    end

    style Alpha1 fill:#e6f7ff,stroke:#1890ff
    style Alpha2 fill:#e6f7ff,stroke:#1890ff
    style Alpha3 fill:#e6f7ff,stroke:#1890ff
    style Beta1 fill:#f9f0ff,stroke:#722ed1,shape:circle
    style Beta2 fill:#f9f0ff,stroke:#722ed1,shape:circle
    style Terminal fill:#fffbe6,stroke:#faad14
</code></pre>

<ul>
  <li><strong>增量匹配</strong>：Beta1 会缓存”地区+车龄”的组合结果。后续新增”车价”事实时，只需与 Beta1 的缓存做 Join，不必从头遍历。</li>
</ul>

<h2 id="3-为什么要分-alpha-和-beta-两层">3. 为什么要分 Alpha 和 Beta 两层</h2>

<p>如果不做拆分，每来一条新事实就要把所有规则的所有条件重新算一遍，复杂度是 O(规则数 × 条件数)。</p>

<p>拆成两层后：</p>

<p><strong>Alpha 层做独立过滤</strong>：每个条件单独检查一个属性，结果缓存在 Alpha 内存中。</p>
<ul>
  <li>新事实进来，只触发属性匹配的 Alpha 节点，其他节点不动</li>
  <li>多条规则如果共用同一个条件（比如都要检查 <code class="language-plaintext highlighter-rouge">age &lt; 10</code>），共享同一个 Alpha 节点，只算一次</li>
</ul>

<p><strong>Beta 层做组合连接</strong>：把多个 Alpha 的结果按 ID 做 Join，中间结果缓存在 Beta 内存中。</p>
<ul>
  <li>已经组合好的部分结果不需要重算。比如”地区+车龄”已经 Join 好了，后面”车价”事实到了，直接拿缓存做第二次 Join</li>
  <li>多条规则如果前几个条件相同，共享同一条 Beta 路径，只在分歧点才分叉</li>
</ul>

<p>类比数据库：Alpha 相当于 <code class="language-plaintext highlighter-rouge">WHERE</code> 过滤，Beta 相当于 <code class="language-plaintext highlighter-rouge">JOIN</code> 连接，Alpha/Beta 内存相当于缓存的中间结果。不分层就等于每次全表扫描加嵌套循环 Join；分层后相当于建了索引并缓存中间结果，只对增量部分做计算。</p>

<h2 id="4-编译与匹配推演">4. 编译与匹配推演</h2>

<p>在 Rete 算法中，<strong>事实 (Fact)</strong> 用”三元组”表示：<code class="language-plaintext highlighter-rouge">(标识符 ^ 属性 值)</code>，简写为 <code class="language-plaintext highlighter-rouge">(FactID, Attribute, Value)</code>。</p>

<p>以车辆投保规则为例：</p>

<p><strong>事实集（进入工作内存）：</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">w1: (car1, area, "北京")</code></li>
  <li><code class="language-plaintext highlighter-rouge">w2: (car1, age, 9)</code></li>
  <li><code class="language-plaintext highlighter-rouge">w3: (car1, price, 15000)</code></li>
</ul>

<p><strong>规则条件（LHS / Pattern）：</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">c1</code>: 匹配地区 <code class="language-plaintext highlighter-rouge">(?, area, "北京")</code></li>
  <li><code class="language-plaintext highlighter-rouge">c2</code>: 匹配车龄 <code class="language-plaintext highlighter-rouge">(?, age, &lt;10)</code></li>
  <li><code class="language-plaintext highlighter-rouge">c3</code>: 匹配车价 <code class="language-plaintext highlighter-rouge">(?, price, &gt;10000)</code></li>
</ul>

<hr />

<h3 id="第一阶段创建-rete-网络编译期">第一阶段：创建 Rete 网络（编译期）</h3>

<p>引擎在事实产生前，读取规则库生成 Rete 网络：</p>

<ol>
  <li><strong>RootNode</strong>：创建全局唯一的入口节点，所有事实从这里进入。</li>
  <li><strong>Alpha 1</strong>：取条件 <code class="language-plaintext highlighter-rouge">c1</code>，生成 <code class="language-plaintext highlighter-rouge">AlphaNode1</code> 检查 <code class="language-plaintext highlighter-rouge">attr == area &amp;&amp; value == "北京"</code>，挂在 Root 下，附带 Alpha 内存。</li>
  <li><strong>Alpha 2</strong>：取条件 <code class="language-plaintext highlighter-rouge">c2</code>，生成 <code class="language-plaintext highlighter-rouge">AlphaNode2</code> 检查 <code class="language-plaintext highlighter-rouge">age &lt; 10</code>，同样挂在 Root 下。</li>
  <li><strong>Beta 1</strong>：创建 <code class="language-plaintext highlighter-rouge">BetaNode1</code>，左输入接 <code class="language-plaintext highlighter-rouge">AlphaNode1</code>，右输入接 <code class="language-plaintext highlighter-rouge">AlphaNode2</code>。</li>
  <li><strong>Alpha 3 + Beta 2</strong>：取条件 <code class="language-plaintext highlighter-rouge">c3</code>，生成 <code class="language-plaintext highlighter-rouge">AlphaNode3</code> 检查 <code class="language-plaintext highlighter-rouge">price &gt; 10000</code>；创建 <code class="language-plaintext highlighter-rouge">BetaNode2</code>，左输入接 <code class="language-plaintext highlighter-rouge">BetaNode1</code> 的输出，右输入接 <code class="language-plaintext highlighter-rouge">AlphaNode3</code>。</li>
  <li><strong>Terminal</strong>：将规则动作封装为 <code class="language-plaintext highlighter-rouge">TerminalNode</code>，接在 <code class="language-plaintext highlighter-rouge">BetaNode2</code> 之后。</li>
</ol>

<hr />

<h3 id="第二阶段匹配过程运行时">第二阶段：匹配过程（运行时）</h3>

<p>网络构建完成后，每条新事实进入网络，依次经过 Alpha 过滤（Select）和 Beta 连接（Join）：</p>

<ol>
  <li><strong>插入 <code class="language-plaintext highlighter-rouge">w1: (car1, area, "北京")</code></strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">w1</code> 从 RootNode 广播到所有 Alpha 节点。</li>
      <li><code class="language-plaintext highlighter-rouge">AlphaNode1</code> 匹配成功（<code class="language-plaintext highlighter-rouge">area == "北京"</code>），将 <code class="language-plaintext highlighter-rouge">w1</code> 存入 Alpha 内存，并通知下游 <code class="language-plaintext highlighter-rouge">BetaNode1</code>。</li>
      <li><code class="language-plaintext highlighter-rouge">AlphaNode2</code>、<code class="language-plaintext highlighter-rouge">AlphaNode3</code> 属性不匹配，丢弃。</li>
    </ul>
  </li>
  <li><strong>插入 <code class="language-plaintext highlighter-rouge">w2: (car1, age, 9)</code></strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">AlphaNode2</code> 匹配成功（<code class="language-plaintext highlighter-rouge">9 &lt; 10</code>），存入内存，通知 <code class="language-plaintext highlighter-rouge">BetaNode1</code>。</li>
      <li><strong>首次 Join</strong>：<code class="language-plaintext highlighter-rouge">BetaNode1</code> 检查左侧（<code class="language-plaintext highlighter-rouge">AlphaNode1</code> 内存中有 <code class="language-plaintext highlighter-rouge">w1</code>）和右侧（<code class="language-plaintext highlighter-rouge">AlphaNode2</code> 内存中有 <code class="language-plaintext highlighter-rouge">w2</code>），两者 ID 均为 <code class="language-plaintext highlighter-rouge">car1</code>，Join 成功，生成组合 <code class="language-plaintext highlighter-rouge">(w1, w2)</code> 存入 Beta 内存，传递给 <code class="language-plaintext highlighter-rouge">BetaNode2</code>。</li>
    </ul>
  </li>
  <li><strong>插入 <code class="language-plaintext highlighter-rouge">w3: (car1, price, 15000)</code></strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">AlphaNode3</code> 匹配成功（<code class="language-plaintext highlighter-rouge">15000 &gt; 10000</code>），存入内存，通知 <code class="language-plaintext highlighter-rouge">BetaNode2</code>。</li>
      <li><strong>二次 Join</strong>：<code class="language-plaintext highlighter-rouge">BetaNode2</code> 左侧已有来自 <code class="language-plaintext highlighter-rouge">BetaNode1</code> 的 <code class="language-plaintext highlighter-rouge">(w1, w2)</code>，右侧新到 <code class="language-plaintext highlighter-rouge">w3</code>，ID 一致，Join 成功，生成完整绑定集 <code class="language-plaintext highlighter-rouge">(w1, w2, w3)</code>。</li>
    </ul>
  </li>
  <li><strong>触发动作</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">(w1, w2, w3)</code> 到达 <code class="language-plaintext highlighter-rouge">TerminalNode</code>，所有条件满足，执行规则动作。</li>
    </ul>
  </li>
</ol>

<h2 id="5-python-代码模拟">5. Python 代码模拟</h2>

<p>下面将 <strong>Root → Alpha → Beta(层1) → Beta(层2) → Terminal</strong> 的网络用 Python 实现，观察数据逐层传递与 Join 组合的过程。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Callable</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Tuple</span><span class="p">,</span> <span class="n">Union</span>

<span class="c1"># ---- 数据结构 ----
</span>
<span class="k">class</span> <span class="nc">Fact</span><span class="p">:</span>
    <span class="s">"""三元组事实: (标识符, 属性, 值)"""</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">attr</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="n">Any</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="nb">id</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="nb">id</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">attr</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">attr</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">value</span><span class="p">:</span> <span class="n">Any</span> <span class="o">=</span> <span class="n">value</span>

    <span class="k">def</span> <span class="nf">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="s">"(</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="nb">id</span><span class="si">}</span><span class="s">, </span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">attr</span><span class="si">}</span><span class="s">, </span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">value</span><span class="si">}</span><span class="s">)"</span>

<span class="c1"># Token 是在网络中流转的绑定集，可以是单个 Fact 或 Fact 元组
</span><span class="n">Token</span> <span class="o">=</span> <span class="n">Union</span><span class="p">[</span><span class="n">Fact</span><span class="p">,</span> <span class="n">Tuple</span><span class="p">[</span><span class="n">Fact</span><span class="p">,</span> <span class="p">...]]</span>

<span class="k">def</span> <span class="nf">token_id</span><span class="p">(</span><span class="n">t</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""从 Token 中提取标识符，用于 Join 时的 ID 一致性校验"""</span>
    <span class="k">return</span> <span class="n">t</span><span class="p">.</span><span class="nb">id</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">Fact</span><span class="p">)</span> <span class="k">else</span> <span class="n">t</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nb">id</span>

<span class="k">def</span> <span class="nf">token_to_tuple</span><span class="p">(</span><span class="n">t</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="n">Fact</span><span class="p">,</span> <span class="p">...]:</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">t</span><span class="p">,)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">Fact</span><span class="p">)</span> <span class="k">else</span> <span class="n">t</span>

<span class="c1"># ---- 节点定义 ----
</span>
<span class="k">class</span> <span class="nc">ReteNode</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">name</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">children</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="s">'ReteNode'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">memory</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">Token</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">def</span> <span class="nf">pass_to_children</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"  [放行] </span><span class="se">\033</span><span class="s">[92m</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="se">\033</span><span class="s">[0m"</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">child</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">children</span><span class="p">:</span>
            <span class="n">child</span><span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">receive</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">pass</span>

<span class="c1"># Alpha 节点：单条件过滤
</span><span class="k">class</span> <span class="nc">AlphaNode</span><span class="p">(</span><span class="n">ReteNode</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">attr</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">condition</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="n">Any</span><span class="p">],</span> <span class="nb">bool</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">attr</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">attr</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">condition</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="n">Any</span><span class="p">],</span> <span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="n">condition</span>

    <span class="k">def</span> <span class="nf">receive</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">Fact</span><span class="p">)</span> <span class="ow">and</span> <span class="n">token</span><span class="p">.</span><span class="n">attr</span> <span class="o">==</span> <span class="bp">self</span><span class="p">.</span><span class="n">attr</span> <span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">condition</span><span class="p">(</span><span class="n">token</span><span class="p">.</span><span class="n">value</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">memory</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">pass_to_children</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>

<span class="c1"># Beta 节点：Join 连接（按标识符做等值连接）
</span><span class="k">class</span> <span class="nc">BetaNode</span><span class="p">(</span><span class="n">ReteNode</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">left_node</span><span class="p">:</span> <span class="n">ReteNode</span><span class="p">,</span> <span class="n">right_node</span><span class="p">:</span> <span class="n">ReteNode</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">left</span><span class="p">:</span> <span class="n">ReteNode</span> <span class="o">=</span> <span class="n">left_node</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">right</span><span class="p">:</span> <span class="n">ReteNode</span> <span class="o">=</span> <span class="n">right_node</span>
        <span class="n">left_node</span><span class="p">.</span><span class="n">children</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
        <span class="n">right_node</span><span class="p">.</span><span class="n">children</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">receive</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="s">"""当左或右有新数据到达时，与对侧内存做 Join"""</span>
        <span class="n">incoming_id</span> <span class="o">=</span> <span class="n">token_id</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>

        <span class="c1"># 在左右两侧中寻找 ID 匹配的已有缓存
</span>        <span class="n">left_matches</span> <span class="o">=</span> <span class="p">[</span><span class="n">m</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">left</span><span class="p">.</span><span class="n">memory</span> <span class="k">if</span> <span class="n">token_id</span><span class="p">(</span><span class="n">m</span><span class="p">)</span> <span class="o">==</span> <span class="n">incoming_id</span><span class="p">]</span>
        <span class="n">right_matches</span> <span class="o">=</span> <span class="p">[</span><span class="n">m</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">right</span><span class="p">.</span><span class="n">memory</span> <span class="k">if</span> <span class="n">token_id</span><span class="p">(</span><span class="n">m</span><span class="p">)</span> <span class="o">==</span> <span class="n">incoming_id</span><span class="p">]</span>

        <span class="k">if</span> <span class="n">left_matches</span> <span class="ow">and</span> <span class="n">right_matches</span><span class="p">:</span>
            <span class="c1"># 将左侧绑定集与右侧绑定集合并为更大的元组
</span>            <span class="n">combined</span> <span class="o">=</span> <span class="n">token_to_tuple</span><span class="p">(</span><span class="n">left_matches</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="n">token_to_tuple</span><span class="p">(</span><span class="n">right_matches</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">memory</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">combined</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">pass_to_children</span><span class="p">(</span><span class="n">combined</span><span class="p">)</span>

<span class="c1"># 终端节点：触发规则动作
</span><span class="k">class</span> <span class="nc">TerminalNode</span><span class="p">(</span><span class="n">ReteNode</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">action_func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="n">Tuple</span><span class="p">[</span><span class="n">Fact</span><span class="p">,</span> <span class="p">...]],</span> <span class="bp">None</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">action</span> <span class="o">=</span> <span class="n">action_func</span>

    <span class="k">def</span> <span class="nf">receive</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">token</span><span class="p">:</span> <span class="n">Token</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">facts</span> <span class="o">=</span> <span class="n">token_to_tuple</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">=&gt; 🎯 【规则激活】</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"   匹配绑定集: </span><span class="si">{</span><span class="n">facts</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">action</span><span class="p">(</span><span class="n">facts</span><span class="p">)</span>

<span class="c1"># ============ 构建 Rete 网络 ============
</span>
<span class="n">root</span> <span class="o">=</span> <span class="n">ReteNode</span><span class="p">(</span><span class="s">"RootNode"</span><span class="p">)</span>

<span class="c1"># [Layer 1] Alpha 节点
</span><span class="n">alpha_area</span> <span class="o">=</span> <span class="n">AlphaNode</span><span class="p">(</span><span class="s">"Alpha[地区=北京]"</span><span class="p">,</span> <span class="s">'area'</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">v</span><span class="p">:</span> <span class="n">v</span> <span class="o">==</span> <span class="s">'北京'</span><span class="p">)</span>
<span class="n">alpha_age</span> <span class="o">=</span> <span class="n">AlphaNode</span><span class="p">(</span><span class="s">"Alpha[车龄&lt;10年]"</span><span class="p">,</span> <span class="s">'age'</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">v</span><span class="p">:</span> <span class="n">v</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">)</span>
<span class="n">alpha_price</span> <span class="o">=</span> <span class="n">AlphaNode</span><span class="p">(</span><span class="s">"Alpha[车价&gt;1万]"</span><span class="p">,</span> <span class="s">'price'</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">v</span><span class="p">:</span> <span class="n">v</span> <span class="o">&gt;</span> <span class="mi">10000</span><span class="p">)</span>
<span class="n">root</span><span class="p">.</span><span class="n">children</span><span class="p">.</span><span class="n">extend</span><span class="p">([</span><span class="n">alpha_area</span><span class="p">,</span> <span class="n">alpha_age</span><span class="p">,</span> <span class="n">alpha_price</span><span class="p">])</span>

<span class="c1"># [Layer 2] Beta1: Join 地区 + 车龄
</span><span class="n">beta_1</span> <span class="o">=</span> <span class="n">BetaNode</span><span class="p">(</span><span class="s">"Beta1[地区+车龄]"</span><span class="p">,</span> <span class="n">alpha_area</span><span class="p">,</span> <span class="n">alpha_age</span><span class="p">)</span>

<span class="c1"># [Layer 3] Beta2: Join Beta1 结果 + 车价
</span><span class="n">beta_2</span> <span class="o">=</span> <span class="n">BetaNode</span><span class="p">(</span><span class="s">"Beta2[合规+车价]"</span><span class="p">,</span> <span class="n">beta_1</span><span class="p">,</span> <span class="n">alpha_price</span><span class="p">)</span>

<span class="c1"># Terminal: 执行动作
</span><span class="n">terminal</span> <span class="o">=</span> <span class="n">TerminalNode</span><span class="p">(</span>
    <span class="s">"Terminal[推荐高价值投保套餐]"</span><span class="p">,</span>
    <span class="k">lambda</span> <span class="n">facts</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"   ===&gt; 执行动作：为 </span><span class="si">{</span><span class="n">facts</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nb">id</span><span class="si">}</span><span class="s"> 推荐北京高价值投保套餐"</span><span class="p">)</span>
<span class="p">)</span>
<span class="n">beta_2</span><span class="p">.</span><span class="n">children</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">terminal</span><span class="p">)</span>

<span class="c1"># ============ 运行测试 ============
</span>
<span class="k">print</span><span class="p">(</span><span class="s">"1. 录入事实：地区..."</span><span class="p">)</span>
<span class="n">root</span><span class="p">.</span><span class="n">pass_to_children</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="s">'car1'</span><span class="p">,</span> <span class="s">'area'</span><span class="p">,</span> <span class="s">'北京'</span><span class="p">))</span>
<span class="c1"># Alpha[地区] 匹配，存入内存；Beta1 右侧为空，阻断
</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">2. 录入事实：车龄..."</span><span class="p">)</span>
<span class="n">root</span><span class="p">.</span><span class="n">pass_to_children</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="s">'car1'</span><span class="p">,</span> <span class="s">'age'</span><span class="p">,</span> <span class="mi">9</span><span class="p">))</span>
<span class="c1"># Alpha[车龄] 匹配；Beta1 左右均有 car1 的数据，Join 成功 → 组合 (w1, w2)
# Beta2 右侧 alpha_price 为空，阻断
</span>
<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">3. 录入事实：车价..."</span><span class="p">)</span>
<span class="n">root</span><span class="p">.</span><span class="n">pass_to_children</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="s">'car1'</span><span class="p">,</span> <span class="s">'price'</span><span class="p">,</span> <span class="mi">15000</span><span class="p">))</span>
<span class="c1"># Alpha[车价] 匹配；Beta2 左侧有 (w1,w2)，右侧有 w3，ID 一致 → Join 成功
# 完整绑定集 (w1, w2, w3) 到达 Terminal，触发动作
</span></code></pre></div></div>

<h2 id="6-优缺点">6. 优缺点</h2>

<h3 id="优点">优点</h3>

<ol>
  <li><strong>规则间共享节点</strong>：不同规则如果有相同的条件模式，会共享 Alpha/Beta 节点。某个节点被 N 条规则共用，该节点上的匹配效率提升 N 倍。</li>
  <li><strong>增量匹配</strong>：Alpha 内存和 Beta 内存缓存了中间结果。事实集变化不大时，大部分节点状态不需要重算，只处理增量部分。</li>
  <li><strong>匹配速度与规则数量无关</strong>：事实在网络中传播时，不满足条件就被拦截，不会继续向下。规则再多，单条事实经过的路径长度是固定的。</li>
</ol>

<h3 id="缺点">缺点</h3>

<ol>
  <li><strong>事实删除开销大</strong>：删除事实时，除了要做和添加相同的计算外，还需要在内存中查找并清除相关的缓存记录。</li>
  <li><strong>内存占用可能指数级增长</strong>：Beta 内存存储的中间组合结果，会随着规则条件数和事实数量增长而膨胀。规则和事实很多时，可能耗尽内存。</li>
</ol>

<h3 id="实践建议">实践建议</h3>

<table>
  <thead>
    <tr>
      <th>建议</th>
      <th>原因</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>容易变化的规则放后面</td>
      <td>减少规则变动导致的网络重建</td>
    </tr>
    <tr>
      <td>约束性强的条件放前面</td>
      <td>尽早过滤，减少进入 Beta 层的数据量</td>
    </tr>
    <tr>
      <td>内存中只存指针，建索引</td>
      <td>缓解 Beta 内存膨胀问题</td>
    </tr>
  </tbody>
</table>

<p>一句话总结：Rete 用空间换时间——通过缓存中间结果加速匹配，代价是内存开销大。适合事实集相对稳定、规则频繁执行的场景。</p>

<h2 id="相关链接">相关链接</h2>

<ul>
  <li>RulEuler：基于 Rete 算法的开源规则引擎：<a href="https://caritasem.github.io/2026/03/ruleuler-rule-engine-introduction/">https://caritasem.github.io/2026/03/ruleuler-rule-engine-introduction/</a></li>
  <li>开源规则引擎实践：<a href="https://github.com/labulakalia/ibm_bak/blob/main/ibm_articles/%E5%BC%80%E6%BA%90%E8%A7%84%E5%88%99%E6%B5%81%E5%BC%95%E6%93%8E%E5%AE%9E%E8%B7%B5.md">https://github.com/labulakalia/ibm_bak/blob/main/ibm_articles/%E5%BC%80%E6%BA%90%E8%A7%84%E5%88%99%E6%B5%81%E5%BC%95%E6%93%8E%E5%AE%9E%E8%B7%B5.md</a></li>
</ul>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="algorithm" /><category term="rete" /><category term="rule-engine" /><category term="python" /><summary type="html"><![CDATA[本文对 Rete 算法的执行架构、网络拓扑、编译与匹配过程进行说明，并附带 Python 代码模拟。]]></summary></entry><entry><title type="html">用 Node.js 实现 Windows RDP 自动登录</title><link href="http://www.caritasem.com/2026/04/windows-rdp-auto-login-with-nodejs/" rel="alternate" type="text/html" title="用 Node.js 实现 Windows RDP 自动登录" /><published>2026-04-02T02:00:00+00:00</published><updated>2026-04-02T02:00:00+00:00</updated><id>http://www.caritasem.com/2026/04/windows-rdp-auto-login-with-nodejs</id><content type="html" xml:base="http://www.caritasem.com/2026/04/windows-rdp-auto-login-with-nodejs/"><![CDATA[<p>一台 Windows 机器上有多个账号，其中一个需要在后台保持登录来跑依赖桌面会话的定时任务。手动用远程桌面连一下太原始，改注册表 <code class="language-plaintext highlighter-rouge">AutoAdminLogon</code> 也只能设默认登录账号，无法在 A 账号已登录的情况下让 B 账号也在后台上线。最终方案：<strong>用 Node.js 在本地跑一个 RDP 客户端，连本机 3389 端口把目标账号登上去。</strong></p>

<!--more-->

<h2 id="原理">原理</h2>

<p>RDP 协议就是远程桌面用的协议，<code class="language-plaintext highlighter-rouge">mstsc.exe</code> 通过它连目标机器、发送凭据、建立会话。这个项目用了基于 <code class="language-plaintext highlighter-rouge">node-rdpjs</code> 改造的协议库，等于用 Node.js 实现了一个精简版 RDP 客户端。连接目标设成 <code class="language-plaintext highlighter-rouge">127.0.0.1:3389</code>（连自己），把用户名密码发过去，Windows 就会为该账号创建会话。登录完成后客户端退出，但会话保持。</p>

<p>再配一个 VBS 脚本把控制台窗口藏掉，扔到开机启动里，就实现了全自动、无感知的后台登录。</p>

<h2 id="使用方式">使用方式</h2>

<h3 id="1-安装依赖">1. 安装依赖</h3>

<p>机器上需要先有 Node.js：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/sibosend/rdp-auto-login.git
<span class="nb">cd </span>rdp-auto-login
npm <span class="nb">install</span>
</code></pre></div></div>

<h3 id="2-修改配置">2. 修改配置</h3>

<p>编辑 <code class="language-plaintext highlighter-rouge">index.js</code>，把用户名密码改成目标账号的：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">protocol</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./protocol</span><span class="dl">'</span><span class="p">);</span>  

<span class="kd">const</span> <span class="nx">c</span> <span class="o">=</span> <span class="nx">protocol</span><span class="p">.</span><span class="nx">rdp</span><span class="p">.</span><span class="nx">createClient</span><span class="p">({</span> 
    <span class="na">domain</span> <span class="p">:</span> <span class="dl">''</span><span class="p">,</span>                  <span class="c1">// 域用户填域名，本地用户留空</span>
    <span class="na">userName</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">your_username</span><span class="dl">'</span><span class="p">,</span>   <span class="c1">// 改成目标账号</span>
    <span class="na">password</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">your_password</span><span class="dl">'</span><span class="p">,</span>   <span class="c1">// 改成对应密码</span>
    <span class="na">enablePerf</span> <span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">autoLogin</span> <span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">decompress</span> <span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="na">screen</span> <span class="p">:</span> <span class="p">{</span> <span class="na">width</span> <span class="p">:</span> <span class="mi">1920</span><span class="p">,</span> <span class="na">height</span> <span class="p">:</span> <span class="mi">1200</span> <span class="p">},</span>
    <span class="na">locale</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">logLevel</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">INFO</span><span class="dl">'</span>
<span class="p">}).</span><span class="nx">connect</span><span class="p">(</span><span class="dl">'</span><span class="s1">127.0.0.1</span><span class="dl">'</span><span class="p">,</span> <span class="mi">3389</span><span class="p">)</span>     <span class="c1">// 连本机，一般不用改</span>
</code></pre></div></div>

<p>改完先跑一下验证：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run start
</code></pre></div></div>

<p>控制台打出 <code class="language-plaintext highlighter-rouge">connected</code> 就说明成功了。打开任务管理器切到”用户”标签页，应该能看到目标账号已在线。</p>

<h3 id="3-打包成-exe">3. 打包成 exe</h3>

<p>生产环境不想装 Node.js，用 <code class="language-plaintext highlighter-rouge">pkg</code> 打包成独立可执行文件：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">-g</span> pkg
pkg <span class="nt">-t</span> node18-win-x64 index.js
<span class="nb">mv </span>index.exe RDP-logon.exe
</code></pre></div></div>

<h3 id="4-后台静默运行">4. 后台静默运行</h3>

<p>直接双击 exe 会弹黑框窗口。项目里带了一个 <code class="language-plaintext highlighter-rouge">RDP-logon.vbs</code>，用 <code class="language-plaintext highlighter-rouge">WScript.Shell</code> 以隐藏窗口方式启动 exe：</p>

<pre><code class="language-vbscript">objShell.Run """" &amp; exePath &amp; ".exe""" &amp; strArguments, 0, False
' 第二个参数 0 = 隐藏窗口，False = 不等待进程结束
</code></pre>

<p>部署步骤：</p>

<ol>
  <li>把 <code class="language-plaintext highlighter-rouge">RDP-logon.exe</code> 和 <code class="language-plaintext highlighter-rouge">RDP-logon.vbs</code> 放同一个目录</li>
  <li><code class="language-plaintext highlighter-rouge">Win+R</code> 输入 <code class="language-plaintext highlighter-rouge">shell:startup</code>，打开启动目录</li>
  <li>把 <code class="language-plaintext highlighter-rouge">RDP-logon.vbs</code>（或其快捷方式）丢进去</li>
</ol>

<p>这样每次宿主账号登录时，VBS 会自动在后台把目标账号登上去。</p>

<h2 id="注意事项">注意事项</h2>

<ul>
  <li><strong>远程桌面必须开启</strong> — 系统属性 → 远程 → 勾选”允许远程连接到此计算机”，否则 3389 端口没在监听。</li>
  <li><strong>多用户并发</strong> — Windows Server 天然支持多会话；Win10/11 专业版默认只允许一个远程会话，需要 RDPWrap 之类的工具解除限制，否则新登录会踢掉当前用户。</li>
  <li><strong>安全性</strong> — 密码明文写在 <code class="language-plaintext highlighter-rouge">index.js</code> 中，打包后不容易直接看到但也不算安全。如有要求，建议加密或从配置文件读取。</li>
</ul>

<h2 id="相关链接">相关链接</h2>

<ul>
  <li>GitHub 仓库：<a href="https://github.com/sibosend/rdp-auto-login">https://github.com/sibosend/rdp-auto-login</a></li>
</ul>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="windows" /><category term="rdp" /><category term="nodejs" /><category term="automation" /><category term="windows" /><summary type="html"><![CDATA[一台 Windows 机器上有多个账号，其中一个需要在后台保持登录来跑依赖桌面会话的定时任务。手动用远程桌面连一下太原始，改注册表 AutoAdminLogon 也只能设默认登录账号，无法在 A 账号已登录的情况下让 B 账号也在后台上线。最终方案：用 Node.js 在本地跑一个 RDP 客户端，连本机 3389 端口把目标账号登上去。]]></summary></entry><entry><title type="html">RulEuler：基于 Rete 算法的开源规则引擎</title><link href="http://www.caritasem.com/2026/03/ruleuler-rule-engine-introduction/" rel="alternate" type="text/html" title="RulEuler：基于 Rete 算法的开源规则引擎" /><published>2026-03-29T02:00:00+00:00</published><updated>2026-03-29T02:00:00+00:00</updated><id>http://www.caritasem.com/2026/03/ruleuler-rule-engine-introduction</id><content type="html" xml:base="http://www.caritasem.com/2026/03/ruleuler-rule-engine-introduction/"><![CDATA[<p>业务系统里大量的 if-else 判断逻辑（风控策略、审批流程、定价规则等）散落在代码中，每次调整都要改代码、测试、发版。<a href="https://sibosend.github.io/ruleuler/">RulEuler</a> 是一个基于 Rete 算法的开源规则引擎，将业务规则从代码中剥离，通过可视化编辑和文本表达式管理规则，实现热更新和独立维护。</p>

<!--more-->

<h2 id="项目背景">项目背景</h2>

<p>RulEuler 基于 URule 2.1.6（Bstek 开源版）二次开发。URule 核心引擎成熟可靠，但 JCR 存储模型对多数开发者不够直观。RulEuler 在保留核心引擎的基础上做了以下改进：</p>

<table>
  <thead>
    <tr>
      <th>改进项</th>
      <th>URule 开源版</th>
      <th>RulEuler</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>存储方式</td>
      <td>仅 JCR（Jackrabbit）</td>
      <td>新增 MySQL 关系表存储</td>
    </tr>
    <tr>
      <td>运行时框架</td>
      <td>Spring Boot 2.x</td>
      <td>Spring Boot 3.x + JDK 21</td>
    </tr>
    <tr>
      <td>管理后台</td>
      <td>jQuery + Bootstrap 3</td>
      <td>React 18 + Ant Design 5</td>
    </tr>
    <tr>
      <td>规则编辑</td>
      <td>操作繁琐</td>
      <td>新增 REA 文本表达式编辑器</td>
    </tr>
    <tr>
      <td>变量定义</td>
      <td>需预定义 Java POJO</td>
      <td>GeneralEntity 动态类型</td>
    </tr>
    <tr>
      <td>权限控制</td>
      <td>无</td>
      <td>RBAC 用户/角色/权限体系</td>
    </tr>
    <tr>
      <td>自动测试</td>
      <td>无</td>
      <td>路径覆盖 + MC/DC 自动生成测试用例</td>
    </tr>
    <tr>
      <td>认证</td>
      <td>无</td>
      <td>JWT 认证</td>
    </tr>
  </tbody>
</table>

<h2 id="适用场景">适用场景</h2>

<ul>
  <li><strong>风控决策</strong> — 信用评分、反欺诈规则、额度审批</li>
  <li><strong>业务审批</strong> — 多级审批流程、条件分支路由</li>
  <li><strong>动态定价</strong> — 促销规则、折扣策略、阶梯计价</li>
  <li><strong>资源分配</strong> — 机位分配、工单派发、负载均衡策略</li>
  <li><strong>合规校验</strong> — 数据校验规则、业务合规检查</li>
</ul>

<h2 id="核心特性">核心特性</h2>

<ul>
  <li><strong>可视化规则编辑</strong> — 决策表、决策树、决策流、评分卡，拖拽式配置</li>
  <li><strong>REA 文本编辑器</strong> — 自然语言风格的表达式编写规则，效率更高</li>
</ul>

<p><img src="https://sibosend.github.io/ruleuler/assets/images/rea.png" alt="REA 文本表达式编辑器" /></p>
<ul>
  <li><strong>Rete 算法驱动</strong> — 高效模式匹配，适合大量规则并发执行</li>
  <li><strong>知识包热更新</strong> — 规则修改后客户端自动加载，无需重启</li>
  <li><strong>自动测试</strong> — 基于路径覆盖自动生成测试用例，回归比对输出变化</li>
</ul>

<p><img src="https://sibosend.github.io/ruleuler/assets/images/test.png" alt="自动测试" /></p>
<ul>
  <li><strong>RBAC 权限控制</strong> — 用户、角色、项目级权限管理</li>
</ul>

<h2 id="架构概览">架构概览</h2>

<p>RulEuler 由两个独立部署的服务组成，通过 MySQL 共享规则数据：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>浏览器 --&gt; 管理后台 (ruleuler-server + ruleuler-admin) :16009
业务系统 --&gt; 规则执行 (ruleuler-client) :16001
管理后台 --&gt; MySQL
规则执行 --&gt; MySQL
</code></pre></div></div>

<ul>
  <li><strong>管理后台</strong> — 规则编辑、项目管理、权限控制</li>
  <li><strong>规则执行</strong> — 加载知识包，执行决策流，对外提供 REST API</li>
</ul>

<h2 id="快速体验">快速体验</h2>

<p>30 秒用 Docker 跑起来：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/sibosend/ruleuler.git
<span class="nb">cd </span>ruleuler
<span class="nb">cp</span> .env.example .env
docker compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>

<p>启动完成后访问 <code class="language-plaintext highlighter-rouge">http://localhost:16009/admin/</code>，<br />
调用规则示例：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:16001/process/airport_gate_allocation_db/gate_pkg/gate_allocation_flow <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
  "FlightInfo": {
    "aircraft_type": "A380",
    "arrival_time": 8,
    "is_international": true,
    "passenger_count": 260
  },
  "GateResult": {}
}'</span>
</code></pre></div></div>

<h2 id="相关链接">相关链接</h2>

<ul>
  <li>项目主页：<a href="https://sibosend.github.io/ruleuler/">https://sibosend.github.io/ruleuler/</a></li>
  <li>GitHub 仓库：<a href="https://github.com/sibosend/ruleuler">https://github.com/sibosend/ruleuler</a></li>
</ul>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="java" /><category term="rule-engine" /><category term="rete" /><category term="spring-boot" /><category term="open-source" /><summary type="html"><![CDATA[业务系统里大量的 if-else 判断逻辑（风控策略、审批流程、定价规则等）散落在代码中，每次调整都要改代码、测试、发版。RulEuler 是一个基于 Rete 算法的开源规则引擎，将业务规则从代码中剥离，通过可视化编辑和文本表达式管理规则，实现热更新和独立维护。]]></summary></entry><entry><title type="html">将 Kiro 的 Claude Opus 模型通过 CC Switch 代理到 Claude Code 使用</title><link href="http://www.caritasem.com/2026/03/kiro-opus-to-claude-code-via-ccswitch/" rel="alternate" type="text/html" title="将 Kiro 的 Claude Opus 模型通过 CC Switch 代理到 Claude Code 使用" /><published>2026-03-13T02:00:00+00:00</published><updated>2026-03-13T02:00:00+00:00</updated><id>http://www.caritasem.com/2026/03/kiro-opus-to-claude-code-via-ccswitch</id><content type="html" xml:base="http://www.caritasem.com/2026/03/kiro-opus-to-claude-code-via-ccswitch/"><![CDATA[<p>Kiro 内置了 Claude Opus 4.6 等模型，但这些模型只能在 Kiro IDE 内使用。本文介绍如何借助 CLIProxyAPI（Plus）和 CC Switch，将 Kiro 的模型 token 代理出来，供 Claude Code（以及 VSCode 中的 Claude Code 扩展）使用。</p>

<!--more-->

<h2 id="原理概述">原理概述</h2>

<p>整体链路如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Kiro IDE 登录 → 本地生成 token 文件
    → CLIProxyAPI Plus 导入 token（--kiro-import）
    → 启动本地代理服务（127.0.0.1:8317）
    → CC Switch 配置 provider 指向该代理
    → Claude Code / VSCode Claude Code 扩展正常使用
</code></pre></div></div>

<p>核心思路：Kiro 登录后会在本地缓存一份 AWS SSO token，CLIProxyAPI Plus 能读取并转换为兼容 Claude API 的代理服务，CC Switch 则负责管理和切换这些配置。</p>

<h2 id="前置准备">前置准备</h2>

<ul>
  <li>已安装并登录过 Kiro IDE（确保本地已生成 token）</li>
  <li>已安装 Claude Code CLI（<code class="language-plaintext highlighter-rouge">npm install -g @anthropic-ai/claude-code</code>）</li>
  <li>已安装 CC Switch（<a href="https://github.com/farion1231/cc-switch">GitHub 下载</a>）</li>
  <li>已下载 CLIProxyAPI Plus（<a href="https://github.com/router-for-me/CLIProxyAPIPlus">GitHub 下载</a>）</li>
</ul>

<h2 id="第一步确认-kiro-token-位置">第一步：确认 Kiro Token 位置</h2>

<p>登录 Kiro IDE 后，token 文件会自动生成在以下路径：</p>

<table>
  <thead>
    <tr>
      <th>系统</th>
      <th>路径</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>macOS</td>
      <td><code class="language-plaintext highlighter-rouge">~/.aws/sso/cache/kiro-auth-token.json</code></td>
    </tr>
    <tr>
      <td>Windows</td>
      <td><code class="language-plaintext highlighter-rouge">C:\Users\&lt;用户名&gt;\.aws\sso\cache\kiro-auth-token.json</code></td>
    </tr>
    <tr>
      <td>Linux</td>
      <td><code class="language-plaintext highlighter-rouge">~/.aws/sso/cache/kiro-auth-token.json</code></td>
    </tr>
  </tbody>
</table>

<p>文件内容大致如下：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"accessToken"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aoa...."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"expiresIn"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
    </span><span class="nl">"refreshToken"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aor...."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tokenType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>如果你从未登录过 Kiro，需要先打开 Kiro IDE 完成一次登录（Google 或 GitHub 均可）。</p>
</blockquote>

<h2 id="第二步配置-cliproxyapi-plus">第二步：配置 CLIProxyAPI Plus</h2>

<h3 id="21-初始化配置文件">2.1 初始化配置文件</h3>

<p>解压 CLIProxyAPI Plus 后，进入目录：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>config.example.yaml config.yaml
</code></pre></div></div>

<p>编辑 <code class="language-plaintext highlighter-rouge">config.yaml</code>，最小配置如下：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">port</span><span class="pi">:</span> <span class="m">8317</span>

<span class="na">auth-dir</span><span class="pi">:</span> <span class="s2">"</span><span class="s">~/.cli-proxy-api"</span>

<span class="na">request-retry</span><span class="pi">:</span> <span class="m">3</span>

<span class="na">quota-exceeded</span><span class="pi">:</span>
  <span class="na">switch-project</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">switch-preview-model</span><span class="pi">:</span> <span class="no">true</span>

<span class="na">api-keys</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">your-custom-api-key"</span>

<span class="na">remote-management</span><span class="pi">:</span>
  <span class="na">allow-remote</span><span class="pi">:</span> <span class="no">false</span>
  <span class="na">secret-key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-management-key"</span>
  <span class="na">disable-control-panel</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<p>其中 <code class="language-plaintext highlighter-rouge">api-keys</code> 是你自定义的密钥，后续 CC Switch 中需要填写。</p>

<h3 id="22-导入-kiro-token">2.2 导入 Kiro Token</h3>

<p>在 CLIProxyAPI Plus 目录下执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># macOS / Linux</span>
./cli-proxy-api-plus <span class="nt">--kiro-import</span>

<span class="c"># Windows</span>
cli-proxy-api-plus.exe <span class="nt">--kiro-import</span>
</code></pre></div></div>

<p>成功后会看到类似输出：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✓ Imported Kiro token from IDE (Provider: )
Authentication saved to /Users/&lt;用户名&gt;/.cli-proxy-api/kiro-imported-xxxx.json
Imported as kiro-imported
Kiro token import successful!
</code></pre></div></div>

<blockquote>
  <p>如果提示找不到 token 文件，请确认 Kiro IDE 已登录，并检查上述路径是否存在 <code class="language-plaintext highlighter-rouge">kiro-auth-token.json</code>。</p>
</blockquote>

<h3 id="23-启动代理服务">2.3 启动代理服务</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># macOS / Linux</span>
./cli-proxy-api-plus

<span class="c"># Windows</span>
cli-proxy-api-plus.exe
</code></pre></div></div>

<p>启动后可访问管理界面：<code class="language-plaintext highlighter-rouge">http://127.0.0.1:8317/management.html</code></p>

<p>在「中心信息」栏目中可以看到已导入的 Kiro 模型列表，包括：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">kiro-claude-opus-4-5-agentic</code></li>
  <li><code class="language-plaintext highlighter-rouge">kiro-claude-sonnet-4-5-agentic</code></li>
  <li><code class="language-plaintext highlighter-rouge">kiro-claude-haiku-4-5-agentic</code></li>
</ul>

<h2 id="第三步在-cc-switch-中配置-provider">第三步：在 CC Switch 中配置 Provider</h2>

<p>打开 CC Switch，点击「Add Provider」，选择自定义配置。</p>

<p><img src="/assets/posts/202603/cc1.png" alt="CC Switch 配置界面" /></p>

<p>关键的环境变量配置如下：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"ANTHROPIC_AUTH_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"your-custom-api-key"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ANTHROPIC_BASE_URL"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://127.0.0.1:8317"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ANTHROPIC_DEFAULT_HAIKU_MODEL"</span><span class="p">:</span><span class="w"> </span><span class="s2">"kiro-claude-haiku-4-5-agentic"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ANTHROPIC_DEFAULT_OPUS_MODEL"</span><span class="p">:</span><span class="w"> </span><span class="s2">"kiro-claude-opus-4-5-agentic"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ANTHROPIC_DEFAULT_SONNET_MODEL"</span><span class="p">:</span><span class="w"> </span><span class="s2">"kiro-claude-sonnet-4-5-agentic"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ANTHROPIC_MODEL"</span><span class="p">:</span><span class="w"> </span><span class="s2">"kiro-claude-opus-4-5-agentic"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>说明：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ANTHROPIC_AUTH_TOKEN</code>：填写你在 <code class="language-plaintext highlighter-rouge">config.yaml</code> 中设置的 <code class="language-plaintext highlighter-rouge">api-keys</code></li>
  <li><code class="language-plaintext highlighter-rouge">ANTHROPIC_BASE_URL</code>：指向 CLIProxyAPI Plus 的本地地址</li>
  <li>模型名称需要使用 Kiro 专属的名称映射（带 <code class="language-plaintext highlighter-rouge">kiro-</code> 前缀），因为 Kiro 的模型名与标准 Claude 模型名不同</li>
</ul>

<p>配置完成后，在 CC Switch 中点击「Enable」激活该 Provider。</p>

<h2 id="第四步在-claude-code-中验证">第四步：在 Claude Code 中验证</h2>

<p>重启终端，运行 Claude Code：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude
</code></pre></div></div>

<p><img src="/assets/posts/202603/cc2.png" alt="Claude Code 使用效果" /></p>

<p>此时 Claude Code 会通过本地代理访问 Kiro 的 Claude Opus 模型。可以用 <code class="language-plaintext highlighter-rouge">/model</code> 命令确认当前使用的模型。</p>

<h2 id="第五步在-vscode-中使用-claude-code-扩展">第五步：在 VSCode 中使用 Claude Code 扩展</h2>

<p>如果你使用 VSCode 的 Claude Code 扩展，同样可以享受到代理后的模型。CC Switch 的配置对 Claude Code CLI 和 VSCode 扩展都生效。</p>

<p><img src="/assets/posts/202603/cc3.png" alt="VSCode 中的 Claude Code" /></p>

<p>在 VSCode 中打开 Claude Code 面板，即可正常使用 Kiro 的 Opus 模型进行编码辅助。</p>

<h2 id="token-过期处理">Token 过期处理</h2>

<p>Kiro 的 token 有效期为 3600 秒（1 小时）。在第一次登录成功后（比如使用 kiro-cli 等方式），CLIProxyAPI 会自动处理每小时的 token 更新，无需手动干预。</p>

<p>如果自动刷新未生效，也可以手动重新导入：</p>

<ol>
  <li>打开 Kiro IDE（会自动刷新 token）</li>
  <li>重新执行 <code class="language-plaintext highlighter-rouge">./cli-proxy-api-plus --kiro-import</code></li>
</ol>

<blockquote>
  <p>前提是 Kiro IDE 保持打开状态，这样 token 会被自动续期。</p>
</blockquote>

<h2 id="常见问题">常见问题</h2>

<p><strong>Q: 导入时提示找不到 token 文件？</strong></p>

<p>确保已打开 Kiro IDE 并完成登录。如果路径不存在，可以手动创建 <code class="language-plaintext highlighter-rouge">~/.aws/sso/cache/</code> 目录，然后将获取到的 token JSON 文件重命名为 <code class="language-plaintext highlighter-rouge">kiro-auth-token.json</code> 放入。</p>

<p><strong>Q: Claude Code 报连接错误？</strong></p>

<p>检查 CLIProxyAPI Plus 是否正在运行，以及端口 8317 是否被占用。可以用 <code class="language-plaintext highlighter-rouge">curl http://127.0.0.1:8317/v1/models</code> 测试连通性。</p>

<p><strong>Q: 模型名称不对？</strong></p>

<p>Kiro 的模型名称带有 <code class="language-plaintext highlighter-rouge">kiro-</code> 前缀和 <code class="language-plaintext highlighter-rouge">-agentic</code> 后缀，必须在 CC Switch 的环境变量中正确映射，否则 Claude Code 会找不到模型。</p>

<h2 id="参考链接">参考链接</h2>

<ul>
  <li><a href="https://linux.do/t/topic/1011966">CLIProxyAPI 系列教程</a>（配置详解）</li>
  <li><a href="https://linux.do/t/topic/1011983">CLIProxyAPI 项目介绍</a></li>
  <li><a href="https://linux.do/t/topic/1033149">中转转发接入篇</a></li>
  <li><a href="https://linux.do/t/topic/1011999">GUI 管理界面</a></li>
  <li><a href="https://linux.do/t/topic/1012017">Docker 部署</a></li>
  <li><a href="https://linux.do/t/topic/1462220">Kiro Token 代理到 CC 的实践记录</a></li>
  <li><a href="https://github.com/farion1231/cc-switch">CC Switch GitHub</a></li>
  <li><a href="https://github.com/router-for-me/CLIProxyAPIPlus">CLIProxyAPI Plus GitHub</a></li>
</ul>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="ai" /><category term="kiro" /><category term="claude-code" /><category term="cc-switch" /><category term="cliproxyapi" /><category term="ai-coding" /><summary type="html"><![CDATA[Kiro 内置了 Claude Opus 4.6 等模型，但这些模型只能在 Kiro IDE 内使用。本文介绍如何借助 CLIProxyAPI（Plus）和 CC Switch，将 Kiro 的模型 token 代理出来，供 Claude Code（以及 VSCode 中的 Claude Code 扩展）使用。]]></summary></entry><entry><title type="html">本地部署 Overleaf + VS Code 打通 + Copilot / Gemini / Claude 辅助写 LaTeX</title><link href="http://www.caritasem.com/2026/01/overleaf-with-ai-support/" rel="alternate" type="text/html" title="本地部署 Overleaf + VS Code 打通 + Copilot / Gemini / Claude 辅助写 LaTeX" /><published>2026-01-03T03:00:07+00:00</published><updated>2026-01-03T03:00:07+00:00</updated><id>http://www.caritasem.com/2026/01/overleaf-with-ai-support</id><content type="html" xml:base="http://www.caritasem.com/2026/01/overleaf-with-ai-support/"><![CDATA[<p>上文（本地化部署 Overleaf）：https://caritasem.github.io/2024/08/latex-on-premise-overleaf-template/</p>

<p>本文记录两件事：</p>
<ul>
  <li>用 VS Code（Overleaf Workshop 插件）把项目同步到本地编辑，并用 Copilot 做润色/改写等 AI 协作。</li>
</ul>

<h2 id="1-vs-code-打通-overleafoverleaf-workshop">1. VS Code 打通 Overleaf（Overleaf Workshop）</h2>

<h3 id="step-1安装插件">step 1：安装插件</h3>

<p>在 VS Code 扩展里搜索并安装：<code class="language-plaintext highlighter-rouge">Overleaf Workshop</code>。</p>

<p><img src="/assets/posts/202601/0.search.png" alt="search-btn" /></p>

<p>类 VS Code 编辑器一般也可用（例如 Antigravity），步骤基本一致。</p>

<h3 id="step-2用-cookie-登录相当于以-co-author-身份接入">step 2：用 Cookie 登录（相当于以 co-author 身份接入）</h3>

<p>插件目前常见的方式是「Login with Cookie」。从网页端 Overleaf 取 Cookie：</p>

<p>1) 打开 Overleaf 网页端，F12 -&gt; DevTools -&gt; Network。<br />
2) 过滤 <code class="language-plaintext highlighter-rouge">/project</code>，然后刷新页面。<br />
3) 点开对应请求，在 Request Headers 里找到 <code class="language-plaintext highlighter-rouge">Cookie</code>，复制完整内容。</p>

<p><img src="/assets/posts/202601/2.cookie.png" alt="cookie" /></p>

<p>回到 VS Code：</p>
<ul>
  <li>Overleaf Workshop 侧边栏 -&gt; <code class="language-plaintext highlighter-rouge">Login with Cookie</code> -&gt; 粘贴 Cookie。</li>
</ul>

<p><img src="/assets/posts/202601/1.cookiebtn.png" alt="cookie-btn" /></p>

<h3 id="step-3同步项目到本地并编译">step 3：同步项目到本地并编译</h3>

<p>登录成功后，插件会列出你的项目（含已删除的项目）。选择一个项目打开，就会把 <code class="language-plaintext highlighter-rouge">.tex</code> 等文件同步到本地工作区。</p>

<p>在本地 <code class="language-plaintext highlighter-rouge">.tex</code> 文件里触发编译即可看到和网页端类似的效果。</p>

<p>编译快捷键（参考文章）：macOS 为 <code class="language-plaintext highlighter-rouge">Option+Command+V</code>（Windows/Linux 按插件默认/自定义为准）。</p>

<p><img src="/assets/posts/202601/3.res.png" alt="result" /></p>

<h3 id="常见问题连的是哪个-overleaf">常见问题：连的是哪个 Overleaf？</h3>

<p>如果你是自建 Overleaf（本地 or 内网），需要在插件/配置里把 Overleaf 服务器地址指向你的服务。</p>

<p><img src="/assets/posts/202601/1.addserver.png" alt="add-server" /></p>

<p>（不同版本的插件入口可能略有差异，核心就是：让插件访问到你的 Overleaf 域名/端口，然后再用 Cookie 做登录。）</p>

<h2 id="2-copilot-辅助写-latex润色改写结构化">2. Copilot 辅助写 LaTeX（润色/改写/结构化）</h2>

<p>有了 VS Code 后，AI 协作基本等价于「对着本地 <code class="language-plaintext highlighter-rouge">.tex</code> 文件用 Copilot」。常用用法：</p>
<ul>
  <li>让 Copilot 帮你把口语化表达改成更学术/更简洁的句式。</li>
  <li>让 Copilot 生成表格/公式骨架（你再补数据）。</li>
  <li>让 Copilot 按某个会议/期刊风格做引用格式微调（仍建议你最终用 bibtex/biblatex 管理）。</li>
</ul>

<h2 id="常用latex-模版">常用latex 模版</h2>
<ul>
  <li><a href="https://github.com/tuna/thuthesis">清华大学论文</a></li>
  <li><a href="https://github.com/sibosend/latex-resume-template">简历模版</a></li>
</ul>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="python" /><category term="latex" /><category term="overleaf" /><category term="template" /><category term="vscode" /><category term="copilot" /><summary type="html"><![CDATA[上文（本地化部署 Overleaf）：https://caritasem.github.io/2024/08/latex-on-premise-overleaf-template/ 本文记录两件事： 用 VS Code（Overleaf Workshop 插件）把项目同步到本地编辑，并用 Copilot 做润色/改写等 AI 协作。 1. VS Code 打通 Overleaf（Overleaf Workshop） step 1：安装插件 在 VS Code 扩展里搜索并安装：Overleaf Workshop。 类 VS Code 编辑器一般也可用（例如 Antigravity），步骤基本一致。 step 2：用 Cookie 登录（相当于以 co-author 身份接入） 插件目前常见的方式是「Login with Cookie」。从网页端 Overleaf 取 Cookie： 1) 打开 Overleaf 网页端，F12 -&gt; DevTools -&gt; Network。 2) 过滤 /project，然后刷新页面。 3) 点开对应请求，在 Request Headers 里找到 Cookie，复制完整内容。 回到 VS Code： Overleaf Workshop 侧边栏 -&gt; Login with Cookie -&gt; 粘贴 Cookie。 step 3：同步项目到本地并编译 登录成功后，插件会列出你的项目（含已删除的项目）。选择一个项目打开，就会把 .tex 等文件同步到本地工作区。 在本地 .tex 文件里触发编译即可看到和网页端类似的效果。 编译快捷键（参考文章）：macOS 为 Option+Command+V（Windows/Linux 按插件默认/自定义为准）。 常见问题：连的是哪个 Overleaf？ 如果你是自建 Overleaf（本地 or 内网），需要在插件/配置里把 Overleaf 服务器地址指向你的服务。 （不同版本的插件入口可能略有差异，核心就是：让插件访问到你的 Overleaf 域名/端口，然后再用 Cookie 做登录。） 2. Copilot 辅助写 LaTeX（润色/改写/结构化） 有了 VS Code 后，AI 协作基本等价于「对着本地 .tex 文件用 Copilot」。常用用法： 让 Copilot 帮你把口语化表达改成更学术/更简洁的句式。 让 Copilot 生成表格/公式骨架（你再补数据）。 让 Copilot 按某个会议/期刊风格做引用格式微调（仍建议你最终用 bibtex/biblatex 管理）。 常用latex 模版 清华大学论文 简历模版]]></summary></entry><entry><title type="html">免费申请域名 SSL 证书, 无限续期，基于 Let’s Encrypt</title><link href="http://www.caritasem.com/2025/11/free-lets-encrypt-automation/" rel="alternate" type="text/html" title="免费申请域名 SSL 证书, 无限续期，基于 Let’s Encrypt" /><published>2025-11-30T03:00:07+00:00</published><updated>2025-11-30T03:00:07+00:00</updated><id>http://www.caritasem.com/2025/11/free-lets-encrypt-automation</id><content type="html" xml:base="http://www.caritasem.com/2025/11/free-lets-encrypt-automation/"><![CDATA[<h2 id="说明">说明</h2>

<p>本文记录单域名（A/普通域名）使用 Certbot 自动签发并自动续期的实施过程，着重讲述基于 <code class="language-plaintext highlighter-rouge">--nginx</code> 的常见流程与注意事项。泛域名（Wildcard）证书需要 DNS-01 验证（DNS API 或手动添加 TXT 记录），不在本文讨论范围。</p>

<h2 id="安装-certbot">安装 Certbot</h2>

<p>以Ubuntu 系统为例：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>certbot python3-certbot-nginx
</code></pre></div></div>

<p>更多安装方式请参考 Certbot 官方文档。</p>

<h2 id="单域名证书申请流程">单域名证书：申请流程</h2>

<p>假设要为 <code class="language-plaintext highlighter-rouge">test.example.com</code> 申请证书，并且 Nginx 已经配置好对应的 <code class="language-plaintext highlighter-rouge">server_name</code> 并能通过 80 端口被外网访问。</p>

<ol>
  <li>使用 nginx 插件自动完成验证与配置（Certbot 会尝试修改 Nginx 配置并在成功后加入 SSL 配置）：</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>certbot <span class="nt">--nginx</span> <span class="nt">-d</span> test.example.com
</code></pre></div></div>

<ol>
  <li>过程要点：
    <ul>
      <li>Certbot 会先验证域名（HTTP-01），要求 80 端口能到达你的 Nginx 服务。</li>
      <li>成功后，Certbot 会在 Nginx 对应的 <code class="language-plaintext highlighter-rouge">server</code> 块中插入由它管理的 <code class="language-plaintext highlighter-rouge">listen 443 ssl</code> / <code class="language-plaintext highlighter-rouge">ssl_certificate</code> / <code class="language-plaintext highlighter-rouge">ssl_certificate_key</code> 等配置；同时可选地添加 80→443 的重定向块。</li>
    </ul>
  </li>
</ol>

<p>示例（申请前 Nginx 简化配置）：</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">upstream</span> <span class="s">backend.gh</span> <span class="p">{</span>
    <span class="kn">server</span> <span class="nf">127.0.0.1</span><span class="p">:</span><span class="mi">3000</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
    <span class="kn">server_name</span> <span class="s">test.example.com</span><span class="p">;</span>

    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
        <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$remote_addr</span><span class="p">;</span>
        <span class="kn">proxy_pass</span> <span class="s">http://backend.gh</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>申请成功后，Certbot 会生成类似的受管理配置（只摘要）：</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>
    <span class="kn">server_name</span> <span class="s">test.example.com</span><span class="p">;</span>

    <span class="kn">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/test.example.com/fullchain.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>
    <span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/test.example.com/privkey.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>
    <span class="kn">include</span> <span class="n">/etc/letsencrypt/options-ssl-nginx.conf</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>
    <span class="kn">ssl_dhparam</span> <span class="n">/etc/letsencrypt/ssl-dhparams.pem</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>

    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
        <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
        <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$remote_addr</span><span class="p">;</span>
        <span class="kn">proxy_pass</span> <span class="s">http://backend.gh</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">server</span> <span class="p">{</span>
    <span class="kn">if</span> <span class="s">(</span><span class="nv">$host</span> <span class="p">=</span> <span class="s">test.example.com)</span> <span class="p">{</span>
        <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>
    <span class="kn">server_name</span> <span class="s">test.example.com</span><span class="p">;</span>
    <span class="kn">return</span> <span class="mi">404</span><span class="p">;</span> <span class="c1"># managed by Certbot</span>
<span class="p">}</span>
</code></pre></div></div>

<p>注意：Certbot 标记为 “managed by Certbot” 的行不应手动删除或随意修改（除非你清楚后果），否则升级或续期时可能出现冲突。</p>

<p>你可以用下面命令查看已安装的证书和路径：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>certbot certificates
</code></pre></div></div>

<h2 id="自动续期">自动续期</h2>

<p>Let’s Encrypt 的证书有效期为 90 天，推荐启用自动续期（Certbot 默认会在安装时创建 systemd timer / cron 任务，取决于安装方式）。常见验证方式：</p>

<ol>
  <li>先做一次模拟续期验证：</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>certbot renew <span class="nt">--dry-run</span>
</code></pre></div></div>

<ol>
  <li>如果模拟续期成功，可以启用自动续期：</li>
</ol>

<ul>
  <li>推荐（现代系统）：使用 systemd timer（通常 Certbot 安装会自动启用）。检查状态：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl list-timers | <span class="nb">grep </span>certbot
</code></pre></div></div>

<ul>
  <li>传统方法：加入 crontab（如果你更习惯或系统没有 systemd）：</li>
</ul>

<pre><code class="language-cron">0 2 * * * * /usr/bin/certbot renew --quiet
</code></pre>

<p>该 cron 表示每日凌晨 02:00 执行一次续期检查。<code class="language-plaintext highlighter-rouge">certbot renew</code> 只有在证书将于 30 天内到期时才会实际尝试续期。</p>

<ol>
  <li>续期后 Certbot 会自动替换 <code class="language-plaintext highlighter-rouge">/etc/letsencrypt/live/&lt;domain&gt;</code> 下的证书文件，通常无需重载 Nginx，因为 Certbot 会在续期时尝试触发必要的 reload。如果你自定义了部署脚本，确保在替换证书后重载 Nginx：</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl reload nginx
</code></pre></div></div>

<h2 id="其他提示">其他提示</h2>

<ul>
  <li>如果你使用代理、Cloudflare 或者负载均衡器，请确保 HTTP-01 挑战能被 LetsEncrypt 的验证服务器访问到（或考虑使用 DNS-01 验证）。</li>
  <li>泛域名（Wildcard）证书需要 DNS-01 验证，通常借助 DNS 提供商 API 自动添加 TXT 记录；此流程和本文不同。</li>
  <li>遇到问题时查看日志：<code class="language-plaintext highlighter-rouge">/var/log/letsencrypt/letsencrypt.log</code>，或在命令行加入 <code class="language-plaintext highlighter-rouge">-v</code> 获取更多调试信息。</li>
</ul>

<hr />
<p>参考：</p>

<ol>
  <li>https://www.cnblogs.com/michaelshen/p/18538178</li>
</ol>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="Tips" /><category term="letsencrypt" /><category term="certbot" /><category term="nginx" /><category term="ssl" /><category term="automation" /><category term="linux" /><summary type="html"><![CDATA[说明 本文记录单域名（A/普通域名）使用 Certbot 自动签发并自动续期的实施过程，着重讲述基于 --nginx 的常见流程与注意事项。泛域名（Wildcard）证书需要 DNS-01 验证（DNS API 或手动添加 TXT 记录），不在本文讨论范围。 安装 Certbot 以Ubuntu 系统为例： sudo apt update sudo apt install certbot python3-certbot-nginx 更多安装方式请参考 Certbot 官方文档。 单域名证书：申请流程 假设要为 test.example.com 申请证书，并且 Nginx 已经配置好对应的 server_name 并能通过 80 端口被外网访问。 使用 nginx 插件自动完成验证与配置（Certbot 会尝试修改 Nginx 配置并在成功后加入 SSL 配置）： sudo certbot --nginx -d test.example.com 过程要点： Certbot 会先验证域名（HTTP-01），要求 80 端口能到达你的 Nginx 服务。 成功后，Certbot 会在 Nginx 对应的 server 块中插入由它管理的 listen 443 ssl / ssl_certificate / ssl_certificate_key 等配置；同时可选地添加 80→443 的重定向块。 示例（申请前 Nginx 简化配置）： upstream backend.gh { server 127.0.0.1:3000; } server { listen 80; server_name test.example.com; location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_pass http://backend.gh; } } 申请成功后，Certbot 会生成类似的受管理配置（只摘要）： server { listen 443 ssl; # managed by Certbot server_name test.example.com; ssl_certificate /etc/letsencrypt/live/test.example.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/test.example.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_pass http://backend.gh; } } server { if ($host = test.example.com) { return 301 https://$host$request_uri; } listen 80; # managed by Certbot server_name test.example.com; return 404; # managed by Certbot } 注意：Certbot 标记为 “managed by Certbot” 的行不应手动删除或随意修改（除非你清楚后果），否则升级或续期时可能出现冲突。 你可以用下面命令查看已安装的证书和路径： sudo certbot certificates 自动续期 Let’s Encrypt 的证书有效期为 90 天，推荐启用自动续期（Certbot 默认会在安装时创建 systemd timer / cron 任务，取决于安装方式）。常见验证方式： 先做一次模拟续期验证： sudo certbot renew --dry-run 如果模拟续期成功，可以启用自动续期： 推荐（现代系统）：使用 systemd timer（通常 Certbot 安装会自动启用）。检查状态： systemctl list-timers | grep certbot 传统方法：加入 crontab（如果你更习惯或系统没有 systemd）： 0 2 * * * * /usr/bin/certbot renew --quiet 该 cron 表示每日凌晨 02:00 执行一次续期检查。certbot renew 只有在证书将于 30 天内到期时才会实际尝试续期。 续期后 Certbot 会自动替换 /etc/letsencrypt/live/&lt;domain&gt; 下的证书文件，通常无需重载 Nginx，因为 Certbot 会在续期时尝试触发必要的 reload。如果你自定义了部署脚本，确保在替换证书后重载 Nginx： sudo systemctl reload nginx 其他提示 如果你使用代理、Cloudflare 或者负载均衡器，请确保 HTTP-01 挑战能被 LetsEncrypt 的验证服务器访问到（或考虑使用 DNS-01 验证）。 泛域名（Wildcard）证书需要 DNS-01 验证，通常借助 DNS 提供商 API 自动添加 TXT 记录；此流程和本文不同。 遇到问题时查看日志：/var/log/letsencrypt/letsencrypt.log，或在命令行加入 -v 获取更多调试信息。 参考： https://www.cnblogs.com/michaelshen/p/18538178]]></summary></entry><entry><title type="html">Installing LightGBM on an M1 Macbook</title><link href="http://www.caritasem.com/2025/11/macbook-arm-lightgbm/" rel="alternate" type="text/html" title="Installing LightGBM on an M1 Macbook" /><published>2025-11-10T03:00:07+00:00</published><updated>2025-11-10T03:00:07+00:00</updated><id>http://www.caritasem.com/2025/11/macbook-arm-lightgbm</id><content type="html" xml:base="http://www.caritasem.com/2025/11/macbook-arm-lightgbm/"><![CDATA[<h1 id="installing-lightgbm-on-an-m1-mac">Installing LightGBM on an M1 Mac</h1>

<p>While setting up LightGBM on my M1 Mac, I discovered that the default <code class="language-plaintext highlighter-rouge">pip install lightgbm</code> command does not work due to compatibility issues with Apple Silicon and <code class="language-plaintext highlighter-rouge">libomp</code> dependencies.</p>

<p>After trying various fixes, including setting environment variables and reinstalling <code class="language-plaintext highlighter-rouge">libomp</code> via Homebrew, I still encountered installation errors with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>lightgbm <span class="nt">--no-binary</span> :all:
</code></pre></div></div>

<p>After some research, I found a solution that worked: creating a new Conda environment and installing LightGBM using Conda-Forge:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>conda <span class="nb">install</span> <span class="se">\</span>
    <span class="nt">--yes</span> <span class="se">\</span>
    <span class="nt">-c</span> conda-forge <span class="se">\</span>
    <span class="s1">'lightgbm&gt;=4.6.0'</span>
</code></pre></div></div>

<p>This approach successfully installed LightGBM without further issues.</p>

<p>For more details and troubleshooting options, I found this Stack Overflow discussion useful.</p>

<p>Ref:</p>

<ol>
  <li>https://katieminjoo.github.io/posts/InstallLGBMonMacM1/</li>
</ol>]]></content><author><name>caritasem</name><email>caritasem@gmail</email></author><category term="Tips" /><category term="macOS" /><category term="Apple Silicon" /><category term="M1" /><category term="LightGBM" /><category term="conda" /><category term="conda-forge" /><summary type="html"><![CDATA[Installing LightGBM on an M1 Mac While setting up LightGBM on my M1 Mac, I discovered that the default pip install lightgbm command does not work due to compatibility issues with Apple Silicon and libomp dependencies. After trying various fixes, including setting environment variables and reinstalling libomp via Homebrew, I still encountered installation errors with: pip install lightgbm --no-binary :all: After some research, I found a solution that worked: creating a new Conda environment and installing LightGBM using Conda-Forge: conda install \ --yes \ -c conda-forge \ 'lightgbm&gt;=4.6.0' This approach successfully installed LightGBM without further issues. For more details and troubleshooting options, I found this Stack Overflow discussion useful. Ref: https://katieminjoo.github.io/posts/InstallLGBMonMacM1/]]></summary></entry></feed>