[{"content":"dsh（DeepSeek Harness）是 DeepSeek 开源的 agent harness，有 CLI 也有 Web UI，默认只监听 127.0.0.1:3080，CLI 明确拒绝 --host 0.0.0.0。它自带启动 token 与签名 cookie 两道鉴权，但没有 TLS、没有账号体系，官方也把「网络部署」列为不支持。想在服务器上用，就得自己在前面架一层。下面是完整过程，机器是 Azure 上那台 892MB 的小 VPS。\n一、为什么不能直接开出去 dsh 的 Web 宿主在 /api 前缀上有一道「浏览器信任栅栏」，代码注释写得很直白：它挡的是 DNS rebinding 和跨站请求，不是身份认证。\n检查 规则 Host 必须是回环地址，或与 --trusted-host 声明的 authority 匹配 Origin（带了才查） 必须与 Host 完全一致 sec-fetch-site cross-site 一律拒 POST 媒体类型 必须是 application/json，否则 415 认证是另一层：每个进程随机生成一个启动 token，只有 GET /?token=... 能把它换成签名 cookie（HttpOnly、SameSite=Strict、绑定 host:port、默认 30 天）。token 不落盘、每次启动都变；签名密钥存在 $DSH_HOME/.credentials.yaml，所以 cookie 能跨重启继续用。\n两条结论直接决定反代怎么写：\nHost 必须原样转发。写成 $host:443 会当场 403 —— 浏览器发来的 Origin 是 https://域名，归一化后不带端口，和 host:443 对不上。 必须传 --trusted-host \u0026lt;域名\u0026gt;，否则 /api 全部被 Host 栅栏拒掉。 二、安装与首启 sudo useradd -m -s /bin/bash dsh # 独立账号，不进 sudo 组 sudo npm i -g @deepseek-ai/dsh # 本次装到 0.1.5-rc.1 首次启动会从内置模板生成 profile（$DSH_HOME/profiles，要装一批插件依赖），所以第一遍慢一些：\nsudo -u dsh env HOME=/home/dsh DSH_HOME=/home/dsh/.dsh \\ dsh web --port 3080 --no-open --trusted-host dsh-xxxx.你的域名 # dsh web: http://127.0.0.1:3080/?token=... --no-open 不弹浏览器，--trusted-host 只扩信任栅栏，不给身份。\n三、systemd 托管 机器只有 892MB 内存，必须给上限；再按 RCE 服务的标准一并做隔离：\n# /etc/systemd/system/dsh-web.service [Unit] Description=DeepSeek Harness (dsh) Web UI After=network-online.target Wants=network-online.target [Service] Type=simple User=dsh Group=dsh WorkingDirectory=/home/dsh/workspace # 工作区就是启动目录 Environment=HOME=/home/dsh Environment=DSH_HOME=/home/dsh/.dsh EnvironmentFile=/etc/dsh/dsh.env # DEEPSEEK_API_KEY，600 root ExecStart=/usr/bin/dsh web --port 3080 --no-open --trusted-host dsh-xxxx.你的域名 Restart=always RestartSec=5 MemoryMax=800M TasksMax=512 NoNewPrivileges=true PrivateTmp=true PrivateDevices=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=/home/dsh ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictSUIDSGID=true LockPersonality=true CapabilityBoundingSet= AmbientCapabilities= RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK UMask=0077 [Install] WantedBy=multi-user.target ProtectSystem=strict 把整个根文件系统挂成只读，唯一能写的是 ReadWritePaths 指定的 /home/dsh。systemd-analyze security dsh-web 实测 4.1 OK。\n四、Key 走环境变量 内置的 llm-deepseek 适配器默认凭据引用就是 DEEPSEEK_API_KEY，用 systemd EnvironmentFile 注入：\n# /etc/dsh/dsh.env（600 root:root） DEEPSEEK_API_KEY=sk-xxxxxxxx 由环境变量供值的引用在 Web 设置里是只读的（writable: false），key 不进应用可写状态，轮换只要改这个文件加一次 restart。\n五、公网入口 为什么用子域名 dsh 前端是标准 SPA，静态资源与 /api 都走绝对路径，用路径前缀反代要动一堆地方，cookie 的 authority 绑定也更绕。子域名最省事；没有域名就用 sslip.io：dsh-xxxx.\u0026lt;IP\u0026gt;.sslip.io 会解析到那个 IP，Let\u0026rsquo;s Encrypt 照签。\n随机子域名本身当第一层\u0026quot;隐藏\u0026quot;：串不可猜。证书用 webroot 签，80 端口的 server 块只放行 ACME 校验：\nlocation ^~ /.well-known/acme-challenge/ { root /var/www/html; auth_basic off; } sudo certbot certonly --webroot -w /var/www/html -d dsh-xxxx.你的域名 # Basic Auth 账号，密码用 openssl 生成 apr1 哈希（服务器上没装 htpasswd 也能用） printf \u0026#39;dsh:%s\\n\u0026#39; \u0026#34;$(openssl passwd -apr1 \u0026#34;$PASS\u0026#34;)\u0026#34; \\ | sudo install -m 640 -o root -g www-data /dev/stdin /etc/nginx/.htpasswd-dsh 反代与限速 server { listen 443 ssl; server_name dsh-xxxx.你的域名; # 只有这个名字能进 auth_basic \u0026#34;DeepSeek Harness\u0026#34;; # 第一道闸 auth_basic_user_file /etc/nginx/.htpasswd-dsh; limit_req zone=dsh_zone burst=120 nodelay; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:3080; proxy_http_version 1.1; proxy_set_header Host $host; # 不带端口，理由见第一节 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_buffering off; # SSE / 流式必需 proxy_read_timeout 3600s; # 长任务别被 60s 掐断 proxy_request_buffering off; } } 六、第二道闸的坑 只过 Basic Auth 会被 dsh 自己挡住：\ndsh web authentication required; reopen the URL printed by dsh web. 这是启动 token 那道闸。手动过的话，从服务日志里捞当前进程的 URL：\njournalctl -u dsh-web -o cat | grep -o \u0026#39;token=[A-Za-z0-9_-]*\u0026#39; | tail -1 两个容易踩的点：\n上游返回的 401 不会触发 error_page：nginx 默认 proxy_intercept_errors off，只拦自己产生的错误码，要在这个 location 里显式打开 map $http_upgrade $connection_upgrade 得定义在 http 块；漏了 WebSocket 升级会失败，页面上表现为一直转圈 七、让闸门无感 token 每进程都在 journal 里，可以让 nginx 自己拿它换 cookie：首页拿到 401 时内部重试一次带 token 的请求，把 303 + Set-Cookie 原样透给浏览器。浏览器地址栏全程不带 token，比手动粘贴 URL 更干净。\nlocation = / { error_page 401 = @dsh_login; proxy_intercept_errors on; # 关键：否则不触发 error_page proxy_pass http://127.0.0.1:3080; } location @dsh_login { proxy_pass http://127.0.0.1:3080/?token=$dsh_launch_token; } $dsh_launch_token 由一个小脚本同步：从 journal 取当前进程 token，写进 /etc/nginx/conf.d/dsh-token.conf 里的 map，再 reload nginx。挂到服务启动钩子上，重启后 token 自动跟上：\n# /etc/systemd/system/dsh-web.service.d/token-sync.conf [Service] ExecStartPost=+/usr/local/bin/dsh-token-sync # + 前缀 = 以 root 跑，才能读 journal、reload nginx 代价要说清楚：这样一来对外实际只剩 Basic Auth 一道闸，加上它前面的随机子域名。想恢复两道闸，屏蔽掉内部重试即可：\nsudo touch /etc/dsh/auto-login.off \u0026amp;\u0026amp; sudo dsh-token-sync --now # 关 sudo rm /etc/dsh/auto-login.off \u0026amp;\u0026amp; sudo dsh-token-sync --now # 开 八、验证 鉴权矩阵，全部从公网入口测：\n请求 结果 无 Basic Auth 401 密码错误 401 Basic Auth、无 cookie 303 + Set-Cookie（自动登录） Basic Auth、带 cookie 200 直连 127.0.0.1:3080 并伪造 Host: localhost 401（信任栅栏与 cookie 双查） 最后用无头浏览器跑一轮真实任务，确认 /api、工具调用、模型都通：\n九、隔离与限制 同机隔离：dsh 能执行任意命令，这台机器上还跑着网关和博客，它的进程不该读到后者的凭据。除 ProtectHome=read-only 外再补一条 ACL，把整个家目录对它关掉：\nsudo setfacl -m u:dsh:--- /home/你的用户 # 撤销：sudo setfacl -x u:dsh /home/你的用户 同机其他服务的家目录里若有 world-readable 的凭据文件（~/.cli-proxy-api/config.yaml、~/.pi、~/.codex），一并收成 700。\n设置页在非回环访问下不可用：客户端会判断页面自身是不是 loopback，不是就把设置持久化降级成内存（源码里是 isLoopback ? 'host' : 'memory'），于是「设置 → 模型」直接报 settings are unavailable in this browser。模型与凭据都改服务端文件：\n# /home/dsh/.dsh/settings.yaml，改完下一次请求生效，不用重启 llm-deepseek: reasoningEffort: high 公网暴露 RCE 的现实：过了 Basic Auth 就是一个能跑命令的 agent。密码用 24 位随机串、限定单域名、开限速，能做的都做了；要更稳就关掉自动登录，只用一次性 token URL 进。\n小结 dsh 默认只回环，反代要原样转发 Host 并传 --trusted-host，否则 /api 被信任栅栏拒掉 上游 401 不触发 error_page，proxy_intercept_errors on 是自动换 cookie 的前提 token 每进程轮换、cookie 签名密钥持久化；用 ExecStartPost=+脚本 把 token 同步给 nginx，重启后进入无感 按 RCE 服务对待：独立用户、只读根、只写自己 home、清空 capability，再用 ACL 挡住同机其他家目录 非回环页面的设置页是禁用的，模型与凭据统一走 settings.yaml 和环境变量 ","date":"2026-09-17T17:00:00+08:00","permalink":"/posts/2026-09-17-dsh-public-web/","title":"把 dsh 挂上公网：两道闸与自动登录"},{"content":"本文整理自 AWS 官方页面 什么是 RAG（检索增强生成）？，内容与观点归 Amazon Web Services 所有，这里做了中文归纳、结构化与配图重绘。\n一、RAG 是什么 检索增强生成（Retrieval-Augmented Generation，RAG）指在生成回答之前，先让大语言模型（LLM）去检索一份权威知识库，用检索到的内容来约束和补充输出。\n模型本身不用重新训练，只是多了一个「查资料」的动作。原文的判断是：这是低成本改进 LLM 输出的方法，让回答在具体场景下保持相关性、准确性和实用性。\n二、为什么需要 RAG LLM 的训练数据是静态的，知识有截止日期；而它的输出又足够自信。原文把这种状态比作一个过于热情的新员工：不了解时事，但什么问题都答得斩钉截铁。\n常见问题 表现 无中生有 没有答案时给出虚假信息 信息过时 用户要最新的具体信息，模型给宽泛的旧内容 来源不权威 依据非权威来源组织回答 术语混淆 不同训练来源对同一术语的定义不同，导致答非所问 RAG 的应对方式是：把模型重定向到预先确定的权威知识来源，组织对输出有更强的控制，用户也能看清答案是怎么来的。\n三、RAG 的四个好处 好处 说明 成本低 针对领域数据重训基础模型的算力和费用都很高；RAG 只要接外部数据，不动模型 信息新鲜 可以直接对接实时社媒、新闻网站等频繁更新的来源 可追溯 输出可以附带引文与来源，用户能自己核对原文，信任度更高 可控 开发者能更换信息来源、按权限限制敏感内容的检索范围，也能在答案引错来源时排错修复 四、RAG 怎么工作 没有 RAG 时，模型只凭训练记忆回答；有了 RAG，流程里多出一个信息检索组件：\n创建外部数据：把 API、数据库、文档库里训练集之外的数据，用嵌入模型转成向量，存进向量数据库，形成模型能理解的知识库。 检索相关信息：把用户查询也转成向量，与向量库匹配，返回最相关的片段。原文的例子是 HR 机器人——员工问「我有多少年假？」，系统会同时检索年假政策文档和该员工的历史休假记录。 增强提示：把检索到的内容拼进提示词，这一步靠提示工程与模型有效沟通，让模型基于「训练知识 + 新知识」作答。 更新外部数据：数据会过期，需要异步更新文档及其嵌入表示，可以走实时流程或定期批处理。 五、RAG 和语义搜索的区别 语义搜索是提升 RAG 效果的一环，适合想把大量外部知识接进 LLM 应用的组织。\n传统/关键字搜索 语义搜索 检索方式 字面匹配 问题映射到相关文档，返回具体文本段落 开发者负担 要自己处理词嵌入、文档分块等准备工序 知识库准备由服务完成 输出 搜索结果列表 语义相关的段落 + 按相关性排序的标记词 原文举的例子：问「去年在机械维修上花了多少钱？」，语义搜索返回的是相关文档中的具体文本，而不是一堆链接，开发者拿这段答案去给 LLM 补充上下文即可。\n六、AWS 上的三种落地方式 方式 说明 Amazon Bedrock 知识库 全托管，几次点击把基础模型接到 RAG 数据源，向量转换、检索、生成都自动处理 Amazon Kendra 机器学习驱动的高精度企业搜索，提供检索 API 与语义排序器，可作为 RAG 工作流的企业检索器 Amazon SageMaker JumpStart 想要更多自定义时用，提供基础模型、内置算法与现成方案，参考 notebook 与示例代码加速实现 Kendra 检索 API 的几个具体能力：单次最多返回 100 个语义相关段落（每段最多 200 个 token，按相关性排序）、提供 S3 / SharePoint / Confluence 等预置连接器、支持 HTML / Word / PowerPoint / PDF / Excel / 纯文本，并能按最终用户权限过滤文档。\n小结 RAG 不改模型，只给模型加一个「先查资料再回答」的检索步骤 它解决的是训练数据静态、模型过度自信带来的四类问题：编造、过时、来源不权威、术语混淆 流程四步：准备外部数据 → 检索相关片段 → 增强提示 → 持续更新数据 语义搜索与 RAG 互补，前者负责把大量非结构化知识变成可检索、可排序的段落 不想自建就用 Amazon Bedrock 知识库，要企业级检索用 Kendra，要深度定制用 SageMaker JumpStart 来源：本文整理自 AWS 官方页面《什么是 RAG（检索增强生成）？》，原文版权归 Amazon Web Services, Inc. 或其关联公司所有；文中观点、案例与数据均引自该页面，配图为本站自制。\n","date":"2026-09-17T15:20:00+08:00","permalink":"/posts/2026-09-17-what-is-rag/","title":"RAG 入门：检索增强生成的原理与 AWS 方案"},{"content":"给 Codex 发一句提示词，模型走服务器上的 CPA 网关（别名 deepseek-flash，推理强度 high），19 分 59 秒后拿到一个能直接双击打开的单文件 3D 天气页。本文放上在线演示、实现要点，以及顺手做的「正文内嵌 HTML」验证。\n一、提示词与产出 提示词只有一句，没有补充任何设计稿或数据：\n项 值 模型 deepseek-flash（CPA 网关别名，codex --profile deepseek，reasoning high） 耗时 19 分 59 秒 产出 单个 index.html，52 KB 运行方式 three.js r160（jsdelivr CDN），无构建、无本地服务器，file:// 直接打开 数据 武汉（30.59°N / 114.31°E）未来 7 天，Open-Meteo 抓取后写死在文件里 二、在线演示 真实可交互页面（WebGL 实时渲染） 全屏打开 ↗ 场景里的星空、镜面地面、泛光都是实时渲染的，不是贴图；演示页需要 WebGL，并从 CDN 拉 three.js。加载不出来时看下面的截图：\n三、数据怎么变成画面 视觉元素 编码的数据 柱体高度 当日气温（18–32℃ 映射到世界坐标） 柱体颜色 温度色带：蓝 → 青 → 绿 → 黄 → 橙 → 红 柱顶亮点 / 底座光环 当日最高温 / 最低温 云团体积 云量（由 WMO 天气代码推出） 雨丝密度 降水量 水平流线速度 风速 跨日发光带 每日温差区间：上沿最高温曲线，下沿最低温 底部日期卡 日期、天气、最高/最低温、温度条 这层映射是模型自己定的，提示词里只给了「炫酷」两个字。\n四、实现要点 单文件结构：\u0026lt;style\u0026gt; + \u0026lt;script type=\u0026quot;importmap\u0026quot;\u0026gt; + \u0026lt;script type=\u0026quot;module\u0026quot;\u0026gt;，没有任何构建步骤 后处理：EffectComposer + UnrealBloomPass 泛光，ACES Filmic 色调映射 地面是 Reflector 实时镜面反射，叠一层自定义着色器网格 云、雨、风、光柱都用自定义 ShaderMaterial / Points，几何体经 mergeGeometries 合并 HUD 是普通 DOM：底部 7 张日期卡、右侧单日明细（含逐小时气温 canvas）、左上信息牌、右上图例 数据写死在文件顶部 DAYS / HOURLY 常量，换城市只改 CITY 与这两个数组 交互：OrbitControls 轨道相机，点柱子或日期卡选中单日，←/→ 切换，空格视角漂移 五、正文内嵌 HTML 的验证 结论：可以。本站的 goldmark 渲染器开着 unsafe = true（config/_default/markup.toml），Markdown 里的原始 HTML 原样输出，上面的演示就是直接嵌进正文的 \u0026lt;iframe\u0026gt;。\n能力 结果 行内标签（\u0026lt;kbd\u0026gt;、\u0026lt;mark\u0026gt;、\u0026lt;figure\u0026gt;） 生效 \u0026lt;style\u0026gt; 内联样式 生效 \u0026lt;iframe\u0026gt; 嵌同站静态页 生效 \u0026lt;details\u0026gt; 折叠块 生效 嵌别的站点 被 X-Frame-Options: SAMEORIGIN 挡掉，只影响别人嵌我们 样式作用域 必须套自己的容器 class，否则会污染整站 演示页放在 static/demos/wuhan-weather-3d/index.html，Hugo 原样复制到站点根目录，nginx 当普通静态页返回。\n展开看这段演示的原始 HTML 写法 \u0026lt;figure class=\u0026#34;demo-embed\u0026#34;\u0026gt; \u0026lt;iframe src=\u0026#34;/demos/wuhan-weather-3d/index.html\u0026#34; title=\u0026#34;武汉未来 7 天 3D 天气可视化（可交互）\u0026#34; loading=\u0026#34;lazy\u0026#34; allowfullscreen\u0026gt;\u0026lt;/iframe\u0026gt; \u0026lt;figcaption class=\u0026#34;demo-bar\u0026#34;\u0026gt; \u0026lt;span\u0026gt;真实可交互页面（WebGL 实时渲染）\u0026lt;/span\u0026gt; \u0026lt;a href=\u0026#34;/demos/wuhan-weather-3d/index.html\u0026#34; target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noopener\u0026#34;\u0026gt;全屏打开 ↗\u0026lt;/a\u0026gt; \u0026lt;/figcaption\u0026gt; \u0026lt;/figure\u0026gt; 样式写在 assets/scss/custom.scss 的 .demo-embed 里，iframe 用 aspect-ratio 控制高度。注意主题给 figure 加了铺满卡片的负外边距：自定义样式只改上下间距、别用 margin 简写覆盖，否则外边距被重置、加宽的 width 还在，块就会向右溢出被裁切。\n小结 一句提示词 + 推理强度 high 的模型，20 分钟拿到能用的单文件可视化页面 效果取决于数据映射想没想清楚：柱高对气温、云量对云团体积、降水对雨丝密度 产物零依赖、零构建，file:// 双击即开，部署时当一个静态文件丢出去即可 博客正文支持原始 HTML：本站 goldmark 已开 unsafe，iframe / style / details 都能用 X-Frame-Options: SAMEORIGIN 只允许嵌本站资源，需要嵌外站就得改响应头 ","date":"2026-09-17T13:10:00+08:00","permalink":"/posts/2026-09-17-codex-3d-weather/","title":"一句话提示词：Codex 写出 3D 天气预报页"},{"content":"同时用几个 AI 上游很麻烦：每个都要单独配 key、单独改客户端，额度也各管各的。用 CLIProxyAPI 在服务器上把它们聚合成一个端点，对外只给一把 key，客户端改个 base_url 就能用，/model 随时切模型。下面是完整部署流程，新服务器按顺序执行即可。\n适用场景 多上游自用：官方 OAuth 与第三方 API 共用一个 base_url，/model 切模型不用改配置；服务器在境外且国内可直连（本文是日本节点）时，客户端也不必自己再挂代理 对外分发：账号额度用不完（比如 GPT Pro 20x），装到服务器上就能公网分发——本机版只能服务自己或局域网，一把 key 给团队 / 朋友，不用再搭 VPN 多客户端复用：Codex、Claude Code、Gemini CLI、Cursor 等共用同一把 key，新增或替换上游时客户端不动 一、准备 项 说明 服务器 能跑 nginx 的 Linux（本文 Ubuntu 24.04），CPA 常驻内存约 57MB 域名 + 证书 反代必须有 HTTPS；没有就用 certbot --nginx -d 你的域名 申请 nginx 反代与鉴权都在这一层 上游账号（可选） ChatGPT / Claude / xAI / Kimi 等任一，用于 OAuth 登录 第三方 Key（可选） DeepSeek 等 OpenAI 兼容服务，官方额度用完后接管 版本：CPA v7.3.4、codex-cli 0.154.0。\n二、安装 CPA cd /tmp curl -fsSL -o cpa.tar.gz \\ https://github.com/router-for-me/CLIProxyAPI/releases/download/v7.3.4/CLIProxyAPI_7.3.4_linux_amd64.tar.gz tar xzf cpa.tar.gz mkdir -p ~/cliproxyapi install -m 0755 cli-proxy-api ~/cliproxyapi/cli-proxy-api 单文件 Go 程序，约 64MB。用 systemd 托管：\n# /etc/systemd/system/cli-proxy-api.service [Unit] Description=CLIProxyAPI - unified upstream gateway After=network-online.target Wants=network-online.target [Service] Type=simple User=azureuser # 改成你的运行用户 WorkingDirectory=/home/azureuser/cliproxyapi ExecStart=/home/azureuser/cliproxyapi/cli-proxy-api -config /home/azureuser/.cli-proxy-api/config.yaml Restart=on-failure RestartSec=3 MemoryMax=384M [Install] WantedBy=multi-user.target sudo systemctl daemon-reload sudo systemctl enable --now cli-proxy-api 三、写配置 ~/.cli-proxy-api/config.yaml：\n# 只监听本机：公网必须先过 nginx host: \u0026#34;127.0.0.1\u0026#34; port: 8317 remote-management: allow-remote: true # 反代场景建议 true secret-key: \u0026#34;管理密钥\u0026#34; # 明文填，启动时自动 bcrypt 加密回写 auth-dir: \u0026#34;/home/azureuser/.cli-proxy-api\u0026#34; # OAuth 凭证目录 api-keys: # 客户端访问 CPA 用的密钥 - \u0026#34;sk-cpa-xxxxxxxx\u0026#34; debug: false 三个关键点：\nhost 绑 127.0.0.1，公网只能从 nginx 进 secret-key 管管理接口，api-keys 管客户端调用，两者别混 上游 provider 全部在管理面板里配，配置文件保持干净 四、登录上游账号 CPA 内置多种 OAuth 登录，按需挑：\n上游 命令 ChatGPT / Codex -codex-device-login（服务器）、-codex-login（本机浏览器） Claude -claude-login xAI -xai-login Kimi -kimi-login Meta -meta-login Devin -devin-login Antigravity -antigravity-login Google Vertex -vertex-import 服务账号.json 以无浏览器的服务器为例，用设备码登录 Codex：\n./cli-proxy-api -config ~/.cli-proxy-api/config.yaml -codex-device-login # Codex device URL: https://auth.openai.com/codex/device # Codex device code: XXXX-XXXX 打开网址输码授权，凭证落到 auth-dir。\n当然也可以直接在 Web 面板里点「认证文件 → 对应上游 → 发起登录」。OAuth 回调端口被占用时用 -oauth-callback-port 指定，无浏览器加 -no-browser。\n⚠️ 不要照搬教程里的 forced_login_method = \u0026quot;api\u0026quot; / preferred_auth_method = \u0026quot;apikey\u0026quot;：已有 ChatGPT 登录态时，Codex 会判定「要用 API key 登录」，登出并删掉 auth.json。加 provider 只靠 env_key。\n五、nginx 反代分发（核心） CPA 只监听本机，对外必须经 nginx。策略：隐藏路径 + Basic Auth + 限速，三条路径分开管面板页、管理 API、客户端 API。\n1. 建账号密码 sudo htpasswd -c /etc/nginx/.htpasswd-cpa cpadmin # 没有 htpasswd 就先装 apache2-utils 2. 写反代片段 # /etc/nginx/snippets/cpa.conf（用 include 引入 server 块） # 限速区（放 http 块；已定义过就跳过） limit_req_zone $binary_remote_addr zone=admin_zone:10m rate=10r/m; location = /cpa-adm-XXXXXXXX { return 301 /cpa-adm-XXXXXXXX/management.html; } location = /cpa-adm-XXXXXXXX/ { return 301 /cpa-adm-XXXXXXXX/management.html; } # ① 面板静态页：Basic Auth + 限速 location ^~ /cpa-adm-XXXXXXXX/ { auth_basic \u0026#34;CPA Admin\u0026#34;; auth_basic_user_file /etc/nginx/.htpasswd-cpa; limit_req zone=admin_zone burst=60 nodelay; proxy_pass http://127.0.0.1:8317/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; # SSE / 流式必需 proxy_read_timeout 3600s; # 长任务别被 60s 掐断 client_max_body_size 200m; } # ② 管理 API：面板默认就调这里，鉴权交给 CPA location ^~ /v0/ { proxy_pass http://127.0.0.1:8317/v0/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_buffering off; } # ③ 客户端 API：隐藏路径 + api-key location ^~ /cpa-api-XXXXXXXX/ { proxy_pass http://127.0.0.1:8317/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_read_timeout 3600s; client_max_body_size 200m; } XXXXXXXX 换成自己的随机串（路径本身就是一层防护）。没有现成 server 块的话，最小骨架：\nserver { listen 443 ssl; server_name 你的域名; ssl_certificate /etc/letsencrypt/live/你的域名/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/你的域名/privkey.pem; include /etc/nginx/snippets/cpa.conf; } 3. 为什么要拆路径 nginx 的 auth_basic 用 Authorization: Basic ... 认证，而 CPA 面板用 Authorization: Bearer \u0026lt;管理密钥\u0026gt; 调 API（management.html 里写死了 Bearer ${managementKey}）。两者共用同一个头：Bearer 会顶掉 Basic，auth_basic 直接 401；用变量喂空值也不会关闭认证（实测返回 realm=\u0026quot;\u0026quot; 的 401）。\n所以面板页归 Basic Auth，管理 API 走一条不带 auth_basic 的路径，由 CPA 自己校验密钥；客户端 API 再走一条隐藏路径：\n路径 用途 鉴权 /cpa-adm-XXXXXXXX/ 面板静态页 Basic Auth /v0/ 管理 API（面板默认调用） CPA 管理密钥（Bearer） /cpa-api-XXXXXXXX/v1/ 客户端 API CPA api-key（Bearer） 4. 面板默认服务地址不带路径 面板取的是 window.location 里不带路径的协议 + 域名 + 端口：\n$f = () =\u0026gt; { const { protocol, hostname, port } = window.location; return Zf(`${protocol}//${hostname}${port ? `:${port}` : \u0026#34;\u0026#34;}`); }; 所以请求会打到 https://域名/v0/management/...（根路径），而不是 /cpa-mgmt-.../ 下的那条。这样配会 404，nginx 日志里能看到 /v0/management/config 没有路由。\n两个解法：面板里手动把服务地址填全，或在 nginx 补一条根路径 /v0/ 路由。上面片段用的是后者。\n改完检查并重载：\nsudo nginx -t \u0026amp;\u0026amp; sudo systemctl reload nginx 六、接入第三方 API（以 DeepSeek 为例） 登录面板（https://域名/cpa-adm-XXXXXXXX/management.html）后，在「提供商」里加一条 OpenAI 兼容：\n字段 值 名称 deepseek base-url https://api.deepseek.com api-key DeepSeek 的 sk-... 模型 deepseek-flash（要更大就再加 deepseek-v4-pro） base-url 带不带 /v1 都行；alias 留空会用 name 当模型名。\n任何 OpenAI 兼容服务同理：填 base-url + api-key 即可，官方 OAuth 与这些第三方模型会一起出现在 /v1/models 里。保存后面板会把配置写回 config.yaml 并热重载，无需重启。\n七、验证 三种协议各测一遍（走公网入口）：\nBASE=https://域名/cpa-api-XXXXXXXX/v1 KEY=sk-cpa-xxxxxxxx # 1) 模型列表（官方 OAuth 与第三方模型的合集） curl -s \u0026#34;$BASE/models\u0026#34; -H \u0026#34;Authorization: Bearer $KEY\u0026#34; # 2) OpenAI 协议 curl -s \u0026#34;$BASE/chat/completions\u0026#34; -H \u0026#34;Authorization: Bearer $KEY\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;model\u0026#34;:\u0026#34;deepseek-flash\u0026#34;,\u0026#34;messages\u0026#34;:[{\u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;回复：A-OK\u0026#34;}]}\u0026#39; # → A-OK # 3) Responses 协议（Codex 用的就是它） curl -s \u0026#34;$BASE/responses\u0026#34; -H \u0026#34;Authorization: Bearer $KEY\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;model\u0026#34;:\u0026#34;deepseek-flash\u0026#34;,\u0026#34;input\u0026#34;:\u0026#34;回复：R-OK\u0026#34;}\u0026#39; 鉴权矩阵（都应满足）：\n面板页 无认证 → 401 面板页 Basic → 200 管理API 正确 Bearer → 200 管理API 错误/无 密钥 → 401 分发API 正确 key → 200 分发API 无 key → 401 八、客户端接入 Codex 指向 CPA（~/.codex/config.toml）：\nmodel_provider = \u0026#34;cpa\u0026#34; model = \u0026#34;gpt-5.6-sol\u0026#34; [model_providers.cpa] name = \u0026#34;CPA\u0026#34; base_url = \u0026#34;http://127.0.0.1:8317/v1\u0026#34; experimental_bearer_token = \u0026#34;sk-cpa-xxxxxxxx\u0026#34; wire_api = \u0026#34;responses\u0026#34; supports_websockets = true /model 里会同时出现官方 OAuth 的模型和第三方模型，额度用光时切过去，会话不断。\n分发给其他客户端（同一把 key）：\n# OpenAI 兼容 OPENAI_BASE_URL = https://域名/cpa-api-XXXXXXXX/v1 OPENAI_API_KEY = sk-cpa-xxxxxxxx # Claude Code export ANTHROPIC_BASE_URL=\u0026#34;https://域名/cpa-api-XXXXXXXX\u0026#34; export ANTHROPIC_AUTH_TOKEN=\u0026#34;sk-cpa-xxxxxxxx\u0026#34; 九、安全与分发建议 CPA 只绑 127.0.0.1，公网必须过 nginx（HTTPS） 面板页 Basic Auth + 管理密钥双因素；管理 API 连续 5 次失败封禁约 30 分钟 客户端 API 只靠 api-key，可随时增删（泄露一个删一个） 隐藏路径 + 限速，降低被扫概率 风险：分发出去的官方模型请求烧的是自己账号的额度，给的人越多越可能封号。分发给不熟的人，建议只发第三方付费模型（按量计费）或加 IP 白名单。\n附录：为什么需要这层网关 /model 弹出的是当前 provider 的模型目录，切的是模型 slug，不是 provider。翻 Codex 源码可确认：ModelPreset 没有 provider 字段。\npub struct ModelPreset { pub id: String, pub model: String, // 只有模型 slug pub display_name: String, // ... 没有 provider } 会话里 provider 固定，想在多个上游之间切只有两条路：退出后用不同 --profile 重开，或把上游合成一个 provider——后者就是第 2–6 节做的事。\n小结 部署顺序：装 CPA → 写配置 → 登录上游 → nginx 反代 → 加第三方 API → 验证 登录方式按上游选：Codex 用设备码，Claude/xAI/Kimi 等用对应 -xxx-login，Vertex 导入服务账号 反代必须拆路径：面板页走 Basic Auth，管理/客户端 API 走 Bearer 面板默认服务地址不带路径，记得补 /v0/ 路由 网关让客户端只配一个 base_url 就能跨上游切换；对外分发的额度风险要提前评估 ","date":"2026-09-17T01:30:00+08:00","permalink":"/posts/2026-09-17-codex-cpa-gateway/","title":"用 CLIProxyAPI 在服务器搭多上游统一网关"},{"content":"Claude Code 默认要登录 Anthropic 官方账号；换成自己的 API 只需要两个变量：ANTHROPIC_BASE_URL 和 ANTHROPIC_AUTH_TOKEN。本文以 DeepSeek 为例（撰写时版本：Claude Code 2.1.178，模型 deepseek-v4-pro[1m] / deepseek-flash）。\n一、最小配置 配置文件位置：\n系统 路径 Linux / macOS / WSL ~/.claude/settings.json Windows C:\\Users\\\u0026lt;用户名\u0026gt;\\.claude\\settings.json 把下面这段抄进 settings.json（BASE_URL、TOKEN、模型名换成自己的）：\n{ \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;https://api.deepseek.com/anthropic\u0026#34;, \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;sk-你的key\u0026#34;, \u0026#34;ANTHROPIC_MODEL\u0026#34;: \u0026#34;deepseek-v4-pro[1m]\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_HAIKU_MODEL\u0026#34;: \u0026#34;deepseek-flash\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_SONNET_MODEL\u0026#34;: \u0026#34;deepseek-flash\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_OPUS_MODEL\u0026#34;: \u0026#34;deepseek-v4-pro[1m]\u0026#34; }, \u0026#34;model\u0026#34;: \u0026#34;haiku\u0026#34;, \u0026#34;includeCoAuthoredBy\u0026#34;: false } 字段含义：\n字段 作用 ANTHROPIC_BASE_URL 请求发到哪：官方、兼容端点或中转站 ANTHROPIC_AUTH_TOKEN 鉴权 token（服务商给的 sk-...） ANTHROPIC_MODEL 默认模型名 ANTHROPIC_DEFAULT_*_MODEL 把 Claude Code 的档位映射到服务商的模型 model 默认用哪一档：haiku / sonnet / opus includeCoAuthoredBy 提交信息里是否带 Claude 署名，一般关掉 二、模型档位映射 Claude Code 内部按档位（不是具体模型名）发请求，2.x 共四档：\n档位 环境变量 用途 fable ANTHROPIC_DEFAULT_FABLE_MODEL 新增档位 haiku ANTHROPIC_DEFAULT_HAIKU_MODEL 后台小任务、快速响应 sonnet ANTHROPIC_DEFAULT_SONNET_MODEL 日常主力 opus ANTHROPIC_DEFAULT_OPUS_MODEL 复杂任务 不映射也能启动，但服务商不认识 claude-* 这类模型名就会报错。DeepSeek 只有两个模型，本机这样映射：\nopus → deepseek-v4-pro[1m] # 复杂任务用 pro sonnet → deepseek-flash haiku → deepseek-flash # 小任务用便宜的 flash 模型名实测：\n名称 结果 deepseek-flash / deepseek-v4-pro ✅ 官方 ID deepseek-v4-pro[1m] ✅ 1M 上下文变体，服务商不支持时去掉后缀 deepseek-v4-flash ✅ 旧别名，仍可用 deepseek-v4.1-flash ❌ 400，不是有效模型名 官方列表可自查：curl -H \u0026quot;Authorization: Bearer $KEY\u0026quot; https://api.deepseek.com/models。\n老配置里常见的 ANTHROPIC_SMALL_FAST_MODEL 等价于 haiku 档。\n三、配置来源与优先级 三个地方都能改，优先级从高到低：\n--settings \u0026lt;file-or-json\u0026gt;：只对当前这条命令生效 ~/.claude/settings.json 的 env 段：最常用，一次配好 Shell 环境变量：export ANTHROPIC_BASE_URL=...，适合临时测试 实测：shell 里给错的 BASE_URL，settings.json 的 env 仍然覆盖。\n--setting-sources user,project,local 可以控制加载哪些来源。\n四、多套配置切换 需要对比不同服务商时，把配置存成不同文件名，切换时用 --settings 指定：\nls ~/.claude/settings.json* # settings.json settings.json.bak settings.json.relay-a settings.json.relay-b claude --settings ~/.claude/settings.json.relay-a # 这次用它跑 要固定切换就做成别名：\n# ~/.bashrc alias claude-d=\u0026#39;claude --settings ~/.claude/settings.json\u0026#39; # 默认：DeepSeek alias claude-a=\u0026#39;claude --settings ~/.claude/settings.json.relay-a\u0026#39; # 备用线路 配置 BASE_URL opus / haiku 映射 默认（DeepSeek） https://api.deepseek.com/anthropic deepseek-v4-pro[1m] / deepseek-flash 中转 A https://中转站域名 claude-opus-4-8 / claude-haiku-4-5 中转 B https://另一个中转站 claude-opus-4-8 / gpt-5.5 五、Windows 端 同样放 %USERPROFILE%\\.claude\\settings.json，结构完全一致。环境变量写法不同：\n# 临时（当前窗口） $env:ANTHROPIC_BASE_URL = \u0026#34;https://api.deepseek.com/anthropic\u0026#34; $env:ANTHROPIC_AUTH_TOKEN = \u0026#34;sk-你的key\u0026#34; # 永久（写入用户环境变量，重开终端生效） setx ANTHROPIC_BASE_URL \u0026#34;https://api.deepseek.com/anthropic\u0026#34; setx ANTHROPIC_AUTH_TOKEN \u0026#34;sk-你的key\u0026#34; 六、验证 claude -p \u0026#34;只回复 ok\u0026#34; # 返回 ok 就通了 交互模式里用 /status 查看当前生效的 BASE_URL 和模型。\n七、常见报错 现象 原因 解法 401 Unauthorized TOKEN 错、过期或没配 重新生成并更新 ANTHROPIC_AUTH_TOKEN 400 / model not found 模型名不在服务商列表 用 /models 接口核对；DeepSeek 只有 deepseek-v4-pro / deepseek-flash 404 / invalid path BASE_URL 少了路径 兼容端点通常有固定路径（DeepSeek 是 /anthropic），按服务商文档填全 缓存相关报错 中转不支持 prompt caching 设 DISABLE_PROMPT_CACHING=1 网关要求额外 header 中转鉴权方式不同 ANTHROPIC_CUSTOM_HEADERS，如 \u0026quot;x-api-key: xxx\u0026quot; 请求发去了奇怪的地方 代理环境变量劫持 检查 HTTP_PROXY / HTTPS_PROXY，必要时 unset 配了 env 仍走官方 交互模式里 /login 登录过官方账号 settings.json 的 env 优先级更高；必要时 /logout 清除登录态 八、安全 key 只放本机：不要提交进仓库，也不要用共享 settings.json 的方式「同步配置」 文件权限：chmod 600 ~/.claude/settings.json 权限白名单放 settings.local.json：本机就是这么分的——settings.json 放 provider 配置，settings.local.json 放工具权限 小结 切 API = 改 ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN，写进 ~/.claude/settings.json 的 env 段 服务商不认 claude-* 模型名时，用 ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL 做映射 模型名以服务商 /models 列表为准，不要凭记忆写 多套配置存成不同文件，claude --settings xxx.json 临时切换 key 不进仓库，文件权限 600 ","date":"2026-09-17T00:30:00+08:00","permalink":"/posts/2026-09-16-claude-code-api/","title":"Claude Code 接入 DeepSeek API"},{"content":"C 盘变红只有两种原因：垃圾太多，或者分区给太小。前者靠清理，后者靠扩容。扩容也可以用 Windows 自带的磁盘管理，但代价通常是先删掉 D 盘；用 DiskGenius 可以无损完成，分两步走。\n本机就是典型场景：一块 954GB 的盘，C 在左（295GB），D 在右（657GB，空着 292GB）。\n一、先判断：清理还是扩容 现象 原因 做法 C 盘 200–300GB，D 盘空一大半 分区分配不合理 扩容（第四节） C 盘 500GB+ 仍然爆红 文件太多：缓存 / 更新残留 / 休眠文件 / 还原点 清理（第三节） 两者都有 — 先清理，再扩容 先看清两个数字：C 盘总容量和实际占用（设置 → 系统 → 存储，或 DiskGenius 的分区信息）。\n二、为什么不用自带的磁盘管理 Windows 自带「磁盘管理 → 扩展卷」有个硬性要求：未分配空间必须紧贴在 C 盘右侧。而 C 右边就是 D，所以自带工具只剩三条路：\n删掉 D → 扩展 C → 重建 D：数据要来回搬，耗时且容易出错 用第三方工具把 D 左侧的空间挪出来 不扩容，继续清理 DiskGenius 做的是第 2 条。\n三、清理：先榨干现有空间 按「释放量 / 风险」排序，以下都安全：\n项目 入口 / 命令 典型释放 说明 Windows 更新缓存 设置 → 存储 → 临时文件，或 cleanmgr 3–20 GB 不要手删 SoftwareDistribution 旧版本组件 Dism /Online /Cleanup-Image /StartComponentCleanup 2–10 GB WinSxS 只能靠它清 上一版系统 设置 → 存储 → 临时文件 → 以前的 Windows 安装 10–30 GB 确认不回滚再删 休眠文件 powercfg /h off ≈ 内存大小 关掉会失去快速启动 系统还原点 vssadmin delete shadows /for=c: /oldest 视情况 保留最近的还原点 浏览器 / 开发缓存 各自设置里清 视情况 npm / pip / uv 的 cache 同理 回收站 右键清空 视情况 — 不要手动删：C:\\Windows、C:\\Program Files、C:\\Users\\...\\AppData 下不认识的目录。\n四、DiskGenius 两步法 入口都在 Partition 菜单：\n前提：C 与 D 在同一物理磁盘，且 C 在左、D 在右（DiskGenius 的磁盘图上能直接看出来）；重要数据先备份。\n第一步：拆分分区，从 D 左侧划出空间 Partition → Split Partition，选 D 盘，从左侧划出想要的大小（例如 100GB）。\n这一步不动 D 的数据，只是把它缩小，并在 C 与 D 之间留出一块「未分配」空间。\n第二步：扩容分区，把空间给 C Partition → Extend Partition，选 C，把右侧的未分配空间并入。\n两步的本质和「直接用 DiskGenius 扩容 C」一样，拆开的好处是：任何一步出问题都能立即停下，不会卡在中间状态。\n操作完成后跑一次 chkdsk C: /f 检查。\n思路参考：知乎《用 DiskGenius 扩充 C 盘空间》。\n五、风险检查清单 检查项 原因 备份重要数据 分区操作有失败概率 BitLocker 先暂停或解密 加密卷上动分区容易出问题 笔记本接电源、关闭休眠 操作中断可能损坏分区表 尽量在 PE 下操作 系统盘正在使用时，部分操作会被占用 关掉其他磁盘工具 避免两个程序同时改分区表 操作后验证 重启、chkdsk、确认 D 盘数据完好 小结 C 盘红了先分清：文件多就清理，分区小才扩容 自带磁盘管理要求「右侧紧邻未分配」，C 在左时基本用不了 DiskGenius 的稳妥姿势是两步：先 Split 出空间，再 Extend 给 C 操作前备份、暂停 BitLocker、接电源；操作后 chkdsk 清理和扩容不冲突：先清理释放，再扩容治本 ","date":"2026-09-16T23:50:00+08:00","permalink":"/posts/2026-09-16-c-drive-full/","title":"C 盘爆红怎么办：先清理，再扩容"},{"content":"这个博客和代理服务跑在一台 Azure 学生订阅的小机器上：日本东部、2 vCPU / 892MB，由 $100 学生额度覆盖。以下是从认证到能用的全过程，数据来自这台机器的实测。\n一、入口：学生认证 Azure for Students：用学校邮箱完成学籍认证，就能拿到 $100 额度 / 年，在读期间每年可续，不需要信用卡。额度可以花在虚拟机、磁盘、流量等几乎所有资源上。\n几点经验：\nedu 邮箱认证最快；没有的话按页面提示走其他学籍验证 免费服务清单里还有一批「12 个月免费」和「永久免费」的小资源，轻量用途基本够 认证通过后先别急着点创建——下一步更重要 二、先查区域：学生订阅有白名单 注意：能列出的区域 ≠ 你能创建的区域。学生订阅常有区域限制，创建时报 RequestDisallowedByAzure，就是被策略拦了。\n# 列出推荐区域 az account list-locations \\ --query \u0026#34;[?metadata.regionCategory==\u0026#39;Recommended\u0026#39;].{name:name, display:displayName}\u0026#34; -o table # 查区域策略限制 az policy assignment list \\ --query \u0026#34;[?contains(properties.displayName,\u0026#39;ocation\u0026#39;)].{name:name, display:properties.displayName}\u0026#34; -o table 选区域的思路：\n目的 区域 国内低延迟 japaneast / koreacentral / southeastasia 美区解锁 westus2 / westus3 这台机器在 japaneast：实测国内直连 443 正常，22 端口会被干扰（TCP 握手能过，SSH 随即被 reset）——这也是本站 SSH 走代理连接的原因。\n三、开机器：B 系列突发型 个人用途首选 B 系列突发型：便宜、平时攒 CPU 积分、闲时省额度。这台用的是：\nVM_SIZE = Standard_B2ats_v2 # 2 vCPU / 1 GiB，约 $8/月 IMAGE = ubuntu-24_04-lts 创建时的三个要点：\nSSH 公钥 + 禁用密码登录（disablePasswordAuthentication=true） NSG 只开必要端口：22（建议限制来源）、80 / 443，代理端口按需 系统盘标准 SSD 就够：61G 实测只用了 4.6G 四、开机后三件事 1. 加 swap（小内存必做） 892MB 内存跑 nginx + Hugo 构建 + 代理，不加 swap 迟早 OOM：\nsudo fallocate -l 2G /swapfile \u0026amp;\u0026amp; sudo chmod 600 /swapfile sudo mkswap /swapfile \u0026amp;\u0026amp; sudo swapon /swapfile echo \u0026#39;/swapfile none swap sw 0 0\u0026#39; | sudo tee -a /etc/fstab 2. 开 BBR 跨境线路建议开 BBR：\necho \u0026#39;net.core.default_qdisc=fq\u0026#39; | sudo tee -a /etc/sysctl.d/99-bbr.conf echo \u0026#39;net.ipv4.tcp_congestion_control=bbr\u0026#39; | sudo tee -a /etc/sysctl.d/99-bbr.conf sudo sysctl --system sysctl net.ipv4.tcp_congestion_control # 应输出 bbr 3. 防火墙：NSG 和系统防火墙只管一层 Azure 的 NSG 在云侧挡流量，系统里还有一层（ufw / iptables）。两层要么都配好、要么明确交给一层。常见问题：NSG 放行了 443，机器里 ufw 却拦着。\n这台机器实测 ufw 是 inactive，访问控制统一交给 NSG。\n五、成本账：$100 能跑多久 B2ats_v2 约 $8/月 → $100 差不多正好一年 只在需要时开机能更久，但注意：「停止」≠ 停止计费，要在门户里选「停止(释放) / deallocate」 公网 IP、磁盘、流量都是单独计费项，删机器时别忘了一起清理 六、踩坑清单 坑 现象 解法 区域白名单 建 VM 报 RequestDisallowedByAzure 换区域，或先跑第二步的查询 Cloud Shell 没建存储 每次重开连不上 VM 首次使用时按提示创建存储账户 NSG 与系统防火墙不一致 端口「明明开了」却不通 两层都查，或统一交给 NSG 国内直连 22 端口 SSH 被 reset（TCP 握手却成功） 走代理连，或换高位端口观察 B 系列 CPU 积分 跑重活突然变慢 它是突发型，别当计算型用 892MB 内存 构建时 OOM 加 swap（见上） 七、这台机器的现状（实测） 它同时跑三件事：sing-box 代理、这个博客（nginx）、Filebrowser 文件管理。日常内存 ~500MB，swap 仅 88MB，不跑重活完全够用。\n参考：linux.do《学生版 Azure 部署教程（含踩坑与解决方法）》，区域白名单和 NSG 的坑记录得更细。\n小结 学生认证是最划算的入口：$100/年、免信用卡，够一台小机器跑一整年 动手前先查区域白名单：能列出来 ≠ 能创建 B 系列突发型 + swap + BBR 是小机器的标准三件套 NSG 和系统防火墙只管一层，并记住是哪一层 不用时记得 deallocate，否则额度会静默消耗 ","date":"2026-09-16T23:30:00+08:00","permalink":"/posts/2026-09-16-azure-student-vps/","title":"白嫖 Azure 学生订阅：$100 额度 + 一台常驻 VPS"},{"content":"博客和代理跑在同一台 Azure 小机器上，改文件只能 ssh + vim。于是把它的家目录挂成本地盘：WSL 里是普通路径，Windows 里是盘符。\n场景 方案 终端 / VS Code WSL：~/remote/vm（sshfs + systemd，断线自愈） 资源管理器 Windows：Z: 盘（rclone + WinFsp） 前提：这台机器直连不通——TCP 握手能通，SSH 立刻被 reset，所有流量必须走本机 clash 代理（127.0.0.1:7890）。\nWSL 侧：sshfs + systemd 自愈 sshfs 协议成熟、支持断线重连，用起来和本地路径没差别。先让 SSH 走代理：\nHost proxy-vm HostName 20.48.50.55 User azureuser IdentityFile ~/.ssh/id_rsa ProxyCommand nc -X connect -x 127.0.0.1:7890 %h %p 再交给 systemd user 服务托管，断线自动重挂：\n# ~/.config/systemd/user/azure-proxy-vm-mount.service [Unit] Description=Mount Azure proxy-vm home via SSHFS StartLimitIntervalSec=0 [Service] Type=simple ExecStartPre=-/usr/bin/fusermount3 -uz %h/remote/vm # 清掉残留挂载 ExecStart=/usr/bin/sshfs proxy-vm:/home/azureuser %h/remote/vm -f \\ -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 ExecStop=/usr/bin/fusermount3 -u %h/remote/vm Restart=always RestartSec=10 [Install] WantedBy=default.target systemctl --user enable --now azure-proxy-vm-mount sudo loginctl enable-linger $USER # 不登录也保活 实测：kill -9 后 10 秒内自动恢复。\nWindows 侧：为什么最后选了 rclone 目标：资源管理器里出现一个盘符。\n先试了 SSHFS-Win（WinFsp 上的 sshfs 移植），挂载直接报 read: Connection reset by peer：它不支持 SOCKS 代理，Windows 上也没有 connect / ncat 这类跳板工具，直连又会被 reset。\n换成 rclone：自带 SSH 客户端，一行 socks_proxy 就能走 clash，私钥、known_hosts 校验都支持：\n# %APPDATA%\\rclone\\rclone.conf [azure] type = sftp host = 20.48.50.55 user = azureuser key_file = C:/Users/\u0026lt;用户名\u0026gt;/.ssh/id_rsa socks_proxy = 127.0.0.1:7890 known_hosts_file = C:/Users/\u0026lt;用户名\u0026gt;/.ssh/known_hosts rclone mount azure:/home/azureuser Z: --vfs-cache-mode full 盘符出现，Z:\\ 也能列目录，但资源管理器里没有。\n最大的坑：盘符按登录会话隔离 结论：Windows 的盘符按登录会话隔离。\n量一下进程令牌里的 AuthenticationId（登录会话 ID）：\nWSL 的 interop 进程也在同一个 Windows 会话里，但令牌属于另一个登录会话——从那儿挂载的 Z:，Explorer 看不见。\n（小坑：GetTokenInformation 的 TokenStatistics 类号是 10，不是 12——12 是 TokenSessionId；AuthenticationId 在结构体偏移 8 字节。）\n解法：挂载必须从交互会话启动。两个入口：\n计划任务：登录时自动跑（隐藏窗口） azure-vm-remount.cmd：需要手动重挂时双击一下 至此，Z: 可见、可用、可自启。\n另一个问题：\\\\wsl.localhost 看不到 FUSE 挂载 WSL 里挂好了，但从 \\\\wsl.localhost\\Ubuntu\\... 访问是空的。\n原因：这个入口由 WSL 的 9p 文件服务器提供，只导出 ext4 根文件系统与 /mnt/c 这类 drvfs；FUSE 挂载（sshfs / rclone / NFS / CIFS）一概不导出。这是 WSL 的架构限制，没有开关。\n所以 Windows 端要自己挂一份，不能借 WSL 的挂载。\n日常使用 systemctl --user restart azure-proxy-vm-mount # WSL 挂载卡住时 Z:\\ # Windows 直接访问 C:\\Users\\\u0026lt;用户名\u0026gt;\\bin\\azure-vm-remount.cmd # 重挂 C:\\Users\\\u0026lt;用户名\u0026gt;\\bin\\azure-vm-umount.cmd # 卸载 小结 直连不通就全线走代理：sshfs 用 ProxyCommand，rclone 用 socks_proxy 挂载要有自愈：systemd 的 Restart=always、rclone 的 --vfs-cache-mode 盘符是登录会话级的：务必从交互会话启动挂载，否则 Explorer 看不见 \\\\wsl.localhost 不导出 FUSE 挂载：Windows 端直接另挂一份 ","date":"2026-09-16T22:40:00+08:00","permalink":"/posts/2026-09-16-mount-azure-vm/","title":"把云主机挂成本地盘：WSL + Windows 双端实战"},{"content":"这台博客跑在一台只有 892MB 内存的 Azure 小机器上，同时兼着代理服务。整套方案的第一原则是：轻。以下只讲思路和取舍，代码只放关键几段。\n全貌：写作 → 存储 → 展示 你在浏览器里写作 │ ▼ ┌─────────────────┐ │ Sveltia CMS │ 写作后台（/admin/） │ 文章 + 图片 │ └────────┬────────┘ │ 保存时直接提交 ▼ ┌─────────────────┐ │ GitHub 私有仓库 │ ← 唯一的“真相来源”，也是备份 └────────┬────────┘ │ 每 1 分钟自动拉取 ▼ ┌─────────────────┐ │ 小机器 (VM) │ git pull → Hugo 构建 → nginx 托管 └────────┬────────┘ │ ▼ 访客访问博客 一句话：GitHub 是内容的家，小机器只负责渲染和展示。 好处很直接：内容永远有一份私有仓库备份；机器只负责构建，压力小；换设备也能写。\n选型 Hugo：单文件二进制，整站构建几十毫秒，内存占用几乎可以忽略——在 1GB 小机器上是决定性优势。对比过 Hexo（依赖多）、WordPress（太重）、Ghost（官方建议 1GB 起步）。\nStack 主题：卡片式、自带暗色模式、搜索、归档、目录，社区里最成熟的 Hugo 主题之一。\nSveltia CMS：最初自己写了个网页后台，能用但很土：图片手动传、预览自己写、格式容易出错。换成 Sveltia 后：富文本 + 实时预览、图片拖拽上传、用表单生成 front matter（不会再因格式写错导致文章不显示）、支持草稿和修订历史。它是纯前端（一个 JS 文件从 CDN 加载），不占服务器内存；支持用 GitHub Token 登录，省掉了自建 OAuth 的麻烦。\n为什么不直接用 GitHub Pages？ 内容都在 GitHub 了，为什么不直接用 GitHub Pages 托管？ 有三个现实约束：\n1. 仓库是私有的，Pages 用不了。 免费账号只能从公开仓库发布 Pages，私有仓库发布需要 Pro/Team 付费计划。想省掉这台机器，就得把整个仓库转公开。\n2. github.io 在国内基本不可达。 实测：直连 https://xxxx.github.io/ 超时，而自己的 Azure IP 是通的。GitHub Pages 走 Fastly CDN，国内访问不可靠；换一个「更省事」的方案，结果自己和读者都打不开，没有意义。\n3. 那台小机器本来 24 小时开着。 它同时兼着代理服务，博客只是「蹭」在上面：nginx + 构建，平时内存占用不到 20MB，边际成本约等于零。\n这个架构也没有绑死：内容全在 Git 里，换机器 git clone + 构建就能恢复。哪天仓库转公开、或者 GitHub 访问不再是问题，迁到 Pages 也就十分钟。\n没有域名，怎么上 HTTPS？ 没有域名就签不了证书，而写作后台的密码不能明文传输。取巧的办法是 sslip.io——一个公共通配 DNS 服务，把 IP 写进域名就能用：\n20.48.50.55.sslip.io → 自动解析到 20.48.50.55 有了它，就能走标准 HTTP 验证，用 certbot 签一张浏览器信任的真证书，并自动续期。代价是依赖 sslip.io 这个第三方服务——对个人博客够用。\n核心：GitHub 私有仓库 + 双向同步 写作后台直接往 GitHub 提交，小机器上也可能有本地改动，所以需要双向同步：GitHub → 机器（拉新文章），机器 → GitHub（推本地改动），两个方向处理不好容易冲突。\n做法是一个每分钟跑一次的脚本，逻辑四步：\n先拉取：git pull --rebase --autostash，把远端新文章合并进来 判断要不要重建：只有「有新提交」或「本地有改动」才构建，避免空转 重新构建：Hugo 生成静态页 再推送：本地有改动就提交并推回 GitHub 两个细节：\n加锁：定时任务和手动触发可能撞车，用 flock 保证同一时间只有一个同步在跑 记住上次构建的版本：用标记文件记录上次构建的 commit，精确判断站点是否最新 这正是备份：所有会变化的东西——文章、图片、站点配置——都在 Git 里。换机器 git clone 几分钟复原整站；误删文章 git revert 就能回来；每次改动有完整历史。版本控制本身就是最好的备份。\n顺带搭的几件小事 HTTPS 自动续期：certbot 定时器自动检查、自动续 缓存策略：HTML 每次校验（刷新即最新），带哈希指纹的 CSS/JS 长期缓存 安全加固：后台路径随机化、Basic Auth、登录限速、响应头加固 踩过的坑 坑 现象 解法 未来日期 文章发了但不显示 Hugo 默认不构建未来文章，开 buildFuture 缺少 front matter 随手记的笔记泄漏到公开页面 Hugo 会把它当页面处理，后加了自动告警 并发推送 git 报 remote rejected 同步脚本加 flock 中文文件名 后台打不开 校验规则放宽到支持中文 成本 机器是 Azure 最低配（2 vCPU / 892MB），本来就在跑代理，博客算是「蹭」的；域名 0 元（sslip.io）；证书 0 元（Let\u0026rsquo;s Encrypt）；仓库 0 元（GitHub 私有仓库免费）。平时内存占用不到 20MB。\n小结 用静态站点保证「轻」，用 Git 仓库保证「稳」，用成熟的 CMS 保证「好写」，用自动化把三者串起来。\n它不是最省事的方案（想省事直接用 Ghost 或 WordPress），但在一台很小的机器上、不花钱、有完整掌控权的前提下，这是我能找到的最好平衡。\n","date":"2026-09-16T21:10:00+08:00","image":"/images/uploads/Default.jpg","permalink":"/posts/how-i-built-this-blog/","title":"我是怎么搭这个博客的"}]