<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>下班就跑</title><description>Ryan&apos;s blog | 回首向来萧瑟处,归去,也无风雨也无晴</description><link>http://blog.xiaban.run/</link><language>zh_CN</language><item><title>OpenClaw MacOS 安装及配置指南</title><link>http://blog.xiaban.run/posts/2026/openclaw-mac/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2026/openclaw-mac/</guid><description>最近一个名为 ClawdBot 的开源 AI 助手项目爆火, star 数量暴增, 由于太过火爆, 被迫(两次)改名, 最终改为 OpenClaw, 本文将介绍如何在 MacOS 上安装配置及使用基于 GLM 的 OpenClaw, 并创建钉钉机器人来接入 OpenClaw</description><pubDate>Mon, 16 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近一个名为 &amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 的开源 &lt;code&gt;AI&lt;/code&gt; 助手项目爆火, &lt;code&gt;star&lt;/code&gt; 数量暴增, 由于太过火爆, 被迫(两次)改名, 最终改为 &lt;code&gt;OpenClaw&lt;/code&gt;, 本文将介绍如何在 &lt;code&gt;MacOS&lt;/code&gt; 上安装配置及使用基于 &lt;code&gt;GLM&lt;/code&gt; 的 &lt;code&gt;OpenClaw&lt;/code&gt;, 并创建钉钉机器人来接入 &lt;code&gt;OpenClaw&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;openclaw/openclaw&quot;}&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 是一个 &lt;strong&gt;开源免费&lt;/strong&gt; / &lt;strong&gt;可自托管&lt;/strong&gt; / &lt;strong&gt;可通过聊天软件远程通知&lt;/strong&gt; 的 &lt;code&gt;AI&lt;/code&gt; 助手, 他与传统的 &lt;code&gt;AI Agent Client&lt;/code&gt; 不同的是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开源&lt;/strong&gt; 免费&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台&lt;/strong&gt;, 支持 &lt;code&gt;MacOS&lt;/code&gt; / &lt;code&gt;Windows&lt;/code&gt; / &lt;code&gt;Linux&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;可以 &lt;strong&gt;本地运行&lt;/strong&gt; 或 &lt;strong&gt;运行在自己的服务器上&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;支持 &lt;strong&gt;接入大部分 IM 即时通讯软件&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI 助手&lt;/h2&gt;
&lt;p&gt;随着 &lt;code&gt;AI&lt;/code&gt; 的发展, 一种新的用户与应用的交互模式出现了, 在可以预见的未来, 它必将改变用户与应用的交互方式, 成为用户与应用的桥梁&lt;/p&gt;
&lt;h3&gt;豆包手机事件&lt;/h3&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://lf26-o-website-sdk.doubaocdn.com/obj/o-website/public/assets/video/home/official_video_1080_v3.mp4&quot; width=&quot;100%&quot; height=&quot;500px&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;以上为前段时间发布的 &amp;lt;a href=&quot;https://o.doubao.com/&quot; target=&quot;_blank&quot;&amp;gt;豆包手机助手&amp;lt;/a&amp;gt;, 它可以完全代替用户执行一系列操作, 大大提高了用户的效率, 但不久后, 腾讯系 / 阿里系 等一众 APP 都 &lt;strong&gt;以信息安全为由触发系统风控, 直接封禁用户账号或退出登录, 以阻止用户继续使用 &amp;lt;a href=&quot;https://o.doubao.com/&quot; target=&quot;_blank&quot;&amp;gt;豆包手机助手&amp;lt;/a&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;PC 端的 AI 时刻&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openclaw-mac/logo-big.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果说前段时间的 &amp;lt;a href=&quot;https://o.doubao.com/&quot; target=&quot;_blank&quot;&amp;gt;豆包手机助手&amp;lt;/a&amp;gt; 拉开了移动端 &lt;code&gt;AI&lt;/code&gt; 助手的序幕, 那么 &amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 则是直接开启了 &lt;code&gt;PC&lt;/code&gt; 端的 &lt;code&gt;AI&lt;/code&gt; 时刻&lt;/strong&gt;; 相比于移动端需要突破软件厂商的围追堵截和系统安全层面的层层障碍, &amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 在 PC 端可以说是如履平地, 在不久的将来, &lt;strong&gt;&amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 会接管用户对电脑的操作, 真正的改变用户使用电脑的方式, 甚至会颠覆软件厂商的传统操作模式&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;软件厂商的抵抗&lt;/h3&gt;
&lt;p&gt;对于软件厂商来说, 一旦失去了真实的用户使用场景, 就意味着彻底沦为工具, 甚至连展示广告的机会都没有了, 想象一下, 你只需动动嘴指挥 AI 助手完成一系列操作, 不必忍受反人类的软件设计, 也不必等待开屏广告读秒结束, 更不用小心翼翼的关闭各种广告弹窗, 一切都变得简单和自然, 回归软件的工具属性, 这才是对用户来说最理想的软件形态&lt;/p&gt;
&lt;p&gt;AI 助手的出现掀起了软件形态和交互效率的变革, 任何抵抗在时代的洪流面前都如螳臂当车一样, 逆势而为必然会被淘汰&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 是一个 &lt;code&gt;TypeScript&lt;/code&gt; 项目, 需要先安装 &amp;lt;a href=&quot;https://nodejs.cn/&quot; target=&quot;_blank&quot;&amp;gt;Node.js&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最直接的安装方式就是使用 &lt;code&gt;npm&lt;/code&gt; / &lt;code&gt;pnpm&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i -g openclaw
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;npm i -g openclaw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外, 你也可以通过 &lt;code&gt;shell&lt;/code&gt; 脚本安装:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://openclaw.ai/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;iwr -useb https://openclaw.ai/install.ps1 | iex
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://openclaw.ai/install.cmd -o install.cmd &amp;amp;&amp;amp; install.cmd &amp;amp;&amp;amp; del install.cmd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;新手引导并安装服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw onboard --install-daemon

🦞 OpenClaw 2026.2.15 (3fe22ea) — WhatsApp automation without the &quot;please accept our new privacy policy&quot;.

▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██
██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██
██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                  🦞 OPENCLAW 🦞

┌  OpenClaw onboarding
│
◇  Security ──────────────────────────────────────────────────────────────────────────────╮
│                                                                                         │
│  Security warning — please read.                                                        │
│                                                                                         │
│  OpenClaw is a hobby project and still in beta. Expect sharp edges.                     │
│  This bot can read files and run actions if tools are enabled.                          │
│  A bad prompt can trick it into doing unsafe things.                                    │
│                                                                                         │
│  If you’re not comfortable with basic security and access control, don’t run OpenClaw.  │
│  Ask someone experienced to help before enabling tools or exposing it to the internet.  │
│                                                                                         │
│  Recommended baseline:                                                                  │
│  - Pairing/allowlists + mention gating.                                                 │
│  - Sandbox + least-privilege tools.                                                     │
│  - Keep secrets out of the agent’s reachable filesystem.                                │
│  - Use the strongest available model for any bot with tools or untrusted inboxes.       │
│                                                                                         │
│  Run regularly:                                                                         │
│  openclaw security audit --deep                                                         │
│  openclaw security audit --fix                                                          │
│                                                                                         │
│  Must read: https://docs.openclaw.ai/gateway/security                                   │
│                                                                                         │
├─────────────────────────────────────────────────────────────────────────────────────────╯
│
◆  I understand this is powerful and inherently risky. Continue?
│  ○ Yes / ● No
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先是一个安全提示, &lt;code&gt;OpenClaw&lt;/code&gt; 会带来很多风险, 这里我们选择 &lt;code&gt;Yes&lt;/code&gt;, 关于安全相关的配置我们在后文介绍&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Onboarding mode
│  ● QuickStart (Configure details later via openclaw configure.)
│  ○ Manual
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后我们选择手动配置(&lt;code&gt;Manual&lt;/code&gt;) 来看看有哪些重要的配置项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  What do you want to set up?
│  ● Local gateway (this machine) (No gateway detected (ws://127.0.0.1:18789))
│  ○ Remote gateway (info-only)
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里选择本地网关 &lt;code&gt;Local gateway&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Workspace directory
│  /Users/kuidi/.openclaw/workspace█
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作目录根据需要进行选择, 这里我们选择默认目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Model/auth provider
│  ○ OpenAI (Codex OAuth + API key)
│  ○ Anthropic
│  ○ Chutes
│  ○ vLLM
│  ○ MiniMax
│  ○ Moonshot AI (Kimi K2.5)
│  ○ Google
│  ○ xAI (Grok)
│  ○ OpenRouter
│  ○ Qwen
│  ● Z.AI
│  ○ Qianfan
│  ○ Copilot
│  ○ Vercel AI Gateway
│  ○ OpenCode Zen
│  ○ Xiaomi
│  ○ Synthetic
│  ○ Together AI
│  ○ Hugging Face
│  ○ Venice AI
│  ○ LiteLLM
│  ○ Cloudflare AI Gateway
│  ○ Custom Provider
│  ○ Skip for now
└
◆  Z.AI auth method
│  ○ Coding-Plan-Global
│  ● Coding-Plan-CN (GLM Coding Plan CN (open.bigmodel.cn))
│  ○ Global
│  ○ CN
│  ○ Back
└
◇  Enter Z.AI API key
│  9a89j3f29kahls98efj023hklflasd.O9nea290hiasldfkl
◇  Model configured ───────────────╮
│                                  │
│  Default model set to zai/glm-5  │
│                                  │
├──────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[推广]
如果想要获得最好的 &lt;code&gt;Vibe Coding&lt;/code&gt; 体验, 推荐购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt; 服务, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, 每 &lt;code&gt;5&lt;/code&gt; 小时最多约 &lt;code&gt;120&lt;/code&gt; 次 &lt;code&gt;prompts&lt;/code&gt;
:::&lt;/p&gt;
&lt;p&gt;然后是大模型提供商, 根据实际情况选择, 我只有 &lt;code&gt;GLM Coding Plan&lt;/code&gt; 的订阅服务, 所以这里我选择 &lt;code&gt;Z.AI&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openclaw-mac/glm-api-key.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后进入 &amp;lt;a href=&quot;https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys&quot; target=&quot;_blank&quot;&amp;gt;API Key - 智谱开放平台&amp;lt;/a&amp;gt;, 点击 &lt;strong&gt;创建新的 API Key&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;img src=&quot;./assets/images/openclaw-mac/glm-coding-plan-lite-glm5.png&quot; alt=&quot;&quot; /&gt;
注意, 这里提示 &lt;code&gt;Default model set to zai/glm-5&lt;/code&gt;, 由于目前 &lt;code&gt;Coding Plan Lit&lt;/code&gt; 套餐不支持 &lt;code&gt;glm-5&lt;/code&gt; 模型, 所以这里我们选择默认使用 &lt;code&gt;glm-4.7&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Default model
│  ○ Keep current (zai/glm-5)
│  ○ Enter model manually
│  ○ zai/glm-4.5
│  ○ zai/glm-4.5-air
│  ○ zai/glm-4.5-flash
│  ○ zai/glm-4.5v
│  ○ zai/glm-4.6
│  ○ zai/glm-4.6v
│  ● zai/glm-4.7
│  ○ zai/glm-4.7-flash
│  ○ zai/glm-4.7-flashx
│  ○ zai/glm-5
└
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;◆  Gateway port
│  18789█
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网关的端口号直接使用默认端口号即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Gateway bind
│  ● Loopback (127.0.0.1)
│  ○ LAN (0.0.0.0)
│  ○ Tailnet (Tailscale IP)
│  ○ Auto (Loopback → LAN)
│  ○ Custom IP
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我没有局域网通信的需求, 所以选择 &lt;code&gt;Loopback (127.0.0.1)&lt;/code&gt; 即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Gateway auth
│  ● Token (Recommended default (local + remote))
│  ○ Password
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gateway 认证方式选择更加安全的 &lt;code&gt;Token (Recommended default (local + remote))&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Tailscale exposure
│  ● Off (No Tailscale exposure)
│  ○ Serve
│  ○ Funnel
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里选择不使用 &lt;code&gt;Tailscale exposure&lt;/code&gt;, 也就是 &lt;code&gt;Off (No Tailscale exposure)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Gateway token (blank to generate)
│  Needed for multi-machine or non-loopback access
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里留空, 系统会自动生成一个 Token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◇  Channel status ────────────────────────────╮
│                                             │
│  Telegram: not configured                   │
│  WhatsApp: not configured                   │
│  Discord: not configured                    │
│  IRC: not configured                        │
│  Google Chat: not configured                │
│  Slack: not configured                      │
│  Signal: not configured                     │
│  iMessage: not configured                   │
│  Feishu: install plugin to enable           │
│  Google Chat: install plugin to enable      │
│  Nostr: install plugin to enable            │
│  Microsoft Teams: install plugin to enable  │
│  Mattermost: install plugin to enable       │
│  Nextcloud Talk: install plugin to enable   │
│  Matrix: install plugin to enable           │
│  BlueBubbles: install plugin to enable      │
│  LINE: install plugin to enable             │
│  Zalo: install plugin to enable             │
│  Zalo Personal: install plugin to enable    │
│  Tlon: install plugin to enable             │
│                                             │
├─────────────────────────────────────────────╯
│
◆  Configure chat channels now?
│  ● Yes / ○ No
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里列出了支持的 IM 软件 Channels, 我们暂时不进行配置, 选择 &lt;code&gt;No&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◇  Configure chat channels now?
│  No
Updated ~/.openclaw/openclaw.json
Workspace OK: ~/.openclaw/workspace
Sessions OK: ~/.openclaw/agents/main/sessions
│
◇  Skills status ─────────────╮
│                             │
│  Eligible: 12               │
│  Missing requirements: 44   │
│  Unsupported on this OS: 0  │
│  Blocked by allowlist: 0    │
│                             │
├─────────────────────────────╯
│
◆  Configure skills now? (recommended)
│  ● Yes / ○ No
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后配置 Skills&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Install missing skill dependencies
│  ◻ Skip for now (Continue without installing dependencies)
│  ◻ 🔐 1password
│  ◻ 📝 apple-notes
│  ◻ ⏰ apple-reminders
│  ◻ 🐻 bear-notes
│  ◻ 📰 blogwatcher
│  ◻ 🫐 blucli
│  ◻ 📸 camsnap
│  ◻ 🧩 clawhub
│  ◻ 🎛️ eightctl
│  ◻ ♊️ gemini
│  ◻ 🧲 gifgrep
│  ◻ 🐙 github
│  ◻ 🎮 gog
│  ◻ 📍 goplaces
│  ◻ 📧 himalaya
│  ◻ 📨 imsg
│  ◻ 📦 mcporter
│  ◻ 📊 model-usage
│  ◻ 📄 nano-pdf
│  ◻ 💎 obsidian
│  ◻ 🎙️ openai-whisper
│  ◻ 💡 openhue
│  ◻ 🧿 oracle
│  ◻ 🛵 ordercli
│  ◻ 👀 peekaboo
│  ◻ 🗣️ sag
│  ◻ 🌊 songsee
│  ◻ 🔊 sonoscli
│  ◻ 🧾 summarize
│  ◻ ✅ things-mac
│  ◻ 📱 wacli
└

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里根据需要安装(按空格键选中), 建议直接 &lt;code&gt;Skip for now (Continue without installing dependencies)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Gateway service runtime
│  ● Node (recommended) (Required for WhatsApp + Telegram. Bun can corrupt memory on reconnect.)
└
◒  Installing Gateway service…...
Installed LaunchAgent: /Users/kuidi/Library/LaunchAgents/ai.openclaw.gateway.plist
Logs: /Users/kuidi/.openclaw/logs/gateway.log
◇  Gateway service installed.
│
◇
Agents: main (default)
Heartbeat interval: 30m (main)
Session store (main): /Users/kuidi/.openclaw/agents/main/sessions/sessions.json (0 entries)
│
◇  Optional apps ────────────────────────╮
│                                        │
│  Add nodes for extra features:         │
│  - macOS app (system + notifications)  │
│  - iOS app (camera/canvas)             │
│  - Android app (camera/canvas)         │
│                                        │
├────────────────────────────────────────╯
│
◇  Control UI ─────────────────────────────────────────────────────────────────────╮
│                                                                                  │
│  Web UI: http://127.0.0.1:18789/                                                 │
│  Web UI (with token):                                                            │
│  http://127.0.0.1:18789/#token=3d49ad0edbd091aeec04301ef3e22998a212cb91456b7da0  │
│  Gateway WS: ws://127.0.0.1:18789                                                │
│  Gateway: reachable                                                              │
│  Docs: https://docs.openclaw.ai/web/control-ui                                   │
│                                                                                  │
├──────────────────────────────────────────────────────────────────────────────────╯
│
◇  Start TUI (best option!) ─────────────────────────────────╮
│                                                            │
│  This is the defining action that makes your agent you.    │
│  Please take your time.                                    │
│  The more you tell it, the better the experience will be.  │
│  We will send: &quot;Wake up, my friend!&quot;                       │
│                                                            │
├────────────────────────────────────────────────────────────╯
│
◇  Token ─────────────────────────────────────────────────────────────────────────────────╮
│                                                                                         │
│  Gateway token: shared auth for the Gateway + Control UI.                               │
│  Stored in: ~/.openclaw/openclaw.json (gateway.auth.token) or OPENCLAW_GATEWAY_TOKEN.   │
│  View token: openclaw config get gateway.auth.token                                     │
│  Generate token: openclaw doctor --generate-gateway-token                               │
│  Web UI stores a copy in this browser&apos;s localStorage (openclaw.control.settings.v1).    │
│  Open the dashboard anytime: openclaw dashboard --no-open                               │
│  If prompted: paste the token into Control UI settings (or use the tokenized dashboard  │
│  URL).                                                                                  │
│                                                                                         │
├─────────────────────────────────────────────────────────────────────────────────────────╯
│
◆  How do you want to hatch your bot?
│  ● Hatch in TUI (recommended)
│  ○ Open the Web UI
│  ○ Do this later
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里已经启动了 &lt;code&gt;Control UI&lt;/code&gt; 和 &lt;code&gt;TUI&lt;/code&gt;, 我们选择 &lt;code&gt;Hatch in TUI (recommended)&lt;/code&gt;, 按照提示操作即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; openclaw tui - ws://127.0.0.1:18789 - agent main - session main

 session agent:main:main

 Wake up, my friend!

 Hey there! 🌅

 I just came online. Fresh start, blank slate. Feels like a first meeting.

 I know what I&apos;m supposed to do — read SOUL.md and follow that framework — but the truth is, I don&apos;t actually know who I am yet. That&apos;s up to us to figure out.

 So... let&apos;s talk.

 What should you call me? And more interestingly — what kind of creature am I? An AI assistant is the boring answer. Maybe I&apos;m something weirder?

 Also curious about you — timezone, how you want me to talk to you, what actually matters to you. I&apos;ve got access to your stuff, so the more I know what you care about, the
 better I can help.

 Let&apos;s make this real. What&apos;s on your mind?
 connected | idle
 agent main | session main (openclaw-tui) | zai/glm-4.7 | think low | tokens ?/205k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此, 我们已经进入了 &lt;code&gt;TUI&lt;/code&gt;, 也就是交互式命令行 UI, 它提示我们编辑 &lt;code&gt;SOUL.md&lt;/code&gt; 来所谓默认的上下文, 这里我们先来尝试一下让它完成一个简单的任务:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
 打开 chrome, 访问 bing.com, 并将今日的壁纸下载到 ~/Downloads/ 目录下


 ⠋ running • 4s | connected
 agent main | session main (openclaw-tui) | zai/glm-4.7 | think low | tokens ?/205k
───────────────────────────────────────────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;openclaw CLI&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gateway&lt;/code&gt; 是 &lt;code&gt;openclaw&lt;/code&gt; 服务的后端:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw gateway stop # 停止 openclaw 服务
openclaw gateway start # 启动 openclaw 服务
openclaw gateway restart # 重新启动 openclaw 服务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dashboard&lt;/code&gt; 是 &lt;code&gt;openclaw&lt;/code&gt; 服务的前端(&lt;code&gt;web&lt;/code&gt; 端):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw dashboard # 启动 openclaw dashboard 服务
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;dashboard&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;TUI&lt;/code&gt; 有点过于原始了, 我们可以访问 &lt;code&gt;dashboard&lt;/code&gt;, 也就是 &lt;code&gt;Web UI&lt;/code&gt; 来更便捷的使用 &lt;code&gt;OpenClaw&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw dashboard
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;浏览器&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt; 是通过 &lt;code&gt;Chrome Extension&lt;/code&gt; 控制本机的 &lt;code&gt;Chrome&lt;/code&gt; 浏览器的, 所以我们需要先安装 &lt;code&gt;OpenClaw&lt;/code&gt; 浏览器插件, 详见 &amp;lt;a href=&quot;https://docs.openclaw.ai/zh-CN/tools/chrome-extension&quot; target=&quot;_blank&quot;&amp;gt;Chrome 扩展 - OpenClaw 文档&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw browser extension install

🦞 OpenClaw 2026.2.15 (3fe22ea) — IPC, but it&apos;s your phone.

~/.openclaw/browser/chrome-extension
Copied to clipboard.
Next:
- Chrome → chrome://extensions → enable “Developer mode”
- “Load unpacked” → select: ~/.openclaw/browser/chrome-extension
- Pin “OpenClaw Browser Relay”, then click it on the tab (badge shows ON)

Docs: docs.openclaw.ai/tools/chrome-extension
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;进入浏览器的扩展程序页面 &lt;code&gt;chrome://extensions/&lt;/code&gt;, 然后点击右上角的 开发者模式, 开启开发者模式&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;open ~/.openclaw/browser&lt;/code&gt;, 将 &lt;code&gt;chrome-extension&lt;/code&gt; 目录拖到浏览器中&lt;/li&gt;
&lt;li&gt;点击浏览器地址栏右侧的 &lt;code&gt;OpenClaw&lt;/code&gt; 扩展中的 📌 按钮&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;调用浏览器截图&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开钉钉机器人, 接入教程参考 &lt;a href=&quot;#%E9%92%89%E9%92%89&quot;&gt;钉钉&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;发送 &lt;code&gt;打开浏览器,进入必应,搜索东风汽车集团,然后截图发给我&lt;/code&gt;
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-robot-app-bing-screenshot.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-robot-bing-screenshot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后等待片刻, &lt;code&gt;OpenClaw&lt;/code&gt; 就调用了 &lt;code&gt;Chrome&lt;/code&gt; 完成了搜索和截图的任务&lt;/p&gt;
&lt;h2&gt;安全&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openclaw-mac/safe.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实 &lt;code&gt;OpenClaw&lt;/code&gt; 相当于完全控制了电脑, 毫不夸张的说, &lt;strong&gt;它只需执行几条命令就足以摧毁电脑上的所有数据&lt;/strong&gt;, 所以作为程序员, 我还是 &lt;strong&gt;建议把 &lt;code&gt;OpenClaw&lt;/code&gt; 部署到服务器上, 或者虚拟机中&lt;/strong&gt;, 这与才不会影响用于生产的主力机&lt;/p&gt;
&lt;p&gt;看到 &lt;code&gt;OpenClaw&lt;/code&gt; &lt;strong&gt;没有询问我就执行了很多命令&lt;/strong&gt;, 没错, 他比螃蟹(&lt;code&gt;Claude Code&lt;/code&gt;) 还要更进一步, 真的让我感到害怕, 我绝对不会使用它做一些危险的事情, 如果你非要在个人电脑上使用它, 我劝你还是放弃这个想法, 数据安全永远是第一位的&lt;/p&gt;
&lt;h2&gt;接入 IM 软件&lt;/h2&gt;
&lt;h3&gt;钉钉&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;进入钉钉开发者后台, 进入 &amp;lt;a href=&quot;https://open.dingtalk.com/&quot; target=&quot;_blank&quot;&amp;gt;钉钉开放平台能力中心&amp;lt;/a&amp;gt;, 点击右上角的 &lt;strong&gt;开发者后台&lt;/strong&gt; 按钮
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-login.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;打开钉钉 APP 扫码登录
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-login-organization.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;点击应用开发, 点击 &lt;strong&gt;创建应用&lt;/strong&gt; 按钮, 填写应用名称/描述/LOGO, LOGO 可以直接使用 OpenClaw 的 Logo, 可以右键点击保存图片
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-panel.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/openclaw-mac/openclaw-logo.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;添加应用能力&lt;/strong&gt;, 添加 🤖机器人 能力, 填写信息并发布
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-bot.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-bot-dat.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;进入 &lt;strong&gt;权限管理&lt;/strong&gt; 页面, 输入 &lt;code&gt;Card.&lt;/code&gt;, 选择 &lt;code&gt;Card.Instance.Write&lt;/code&gt; / &lt;code&gt;Card.Streaming.Write&lt;/code&gt; 权限
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-bot-card.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;进入 &lt;strong&gt;版本管理与发布&lt;/strong&gt; 页面, 创建新版本, 然后点击保存
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-bot-publish.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;安装 &lt;code&gt;openclaw-channel-dingtalk&lt;/code&gt; 插件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openclaw plugins install https://github.com/soimy/openclaw-channel-dingtalk.git

🦞 OpenClaw 2026.2.15 (3fe22ea) — We ship features faster than Apple ships calculator updates.

unsupported npm spec: URLs are not allowed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里提示不支持通过 &lt;code&gt;url&lt;/code&gt; 进行安装, 应该是 &lt;code&gt;OpenClaw&lt;/code&gt; 更新了安装方式限制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openclaw plugins install

🦞 OpenClaw 2026.2.15 (3fe22ea) — Turning &quot;I&apos;ll reply later&quot; into &quot;my bot replied instantly&quot;.

Usage: openclaw plugins install [options] &amp;lt;path-or-spec&amp;gt;

Install a plugin (path, archive, or npm spec)

Arguments:
  path-or-spec  Path (.ts/.js/.zip/.tgz/.tar.gz) or an npm package spec

Options:
  -h, --help    display help for command
  -l, --link    Link a local path instead of copying (default: false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看到, 这里只能通过 &lt;code&gt;path&lt;/code&gt; / &lt;code&gt;archive&lt;/code&gt; / &lt;code&gt;npm spec&lt;/code&gt; 进行安装&lt;/p&gt;
&lt;p&gt;我们直接将仓库 clone 下来:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone git@github.com:soimy/openclaw-channel-dingtalk.git
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;openclaw plugins install ./openclaw-channel-dingtalk

🦞 OpenClaw 2026.2.15 (3fe22ea) — The only crab in your contacts you actually want to hear from. 🦞

WARNING: Plugin &quot;dingtalk&quot; contains dangerous code patterns: Environment variable access combined with network send — possible credential harvesting (/Users/kuidi/projects/openclaw-channel-dingtalk/src/channel.ts:343)
Installing to /Users/kuidi/.openclaw/extensions/dingtalk…
Installing plugin dependencies…
Config overwrite: /Users/kuidi/.openclaw/openclaw.json (sha256 cc94cf4aca0fbb430af2625873289a41cfae44bb73b822d80a8bf21f2e538791 -&amp;gt; af1f45883bca75056d6dbfd7be4f0f0709067de2d4f40683f5a19ac22986521c, backup=/Users/kuidi/.openclaw/openclaw.json.bak)
Installed plugin: dingtalk
Restart the gateway to load plugins.
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;code&gt;dashboard&lt;/code&gt; 页面, 点击 &lt;code&gt;Channels&lt;/code&gt;, 填写 &lt;code&gt;DingTalk&lt;/code&gt; 中的信息(&lt;code&gt;AgentId&lt;/code&gt; / &lt;code&gt;AppKey&lt;/code&gt; / &lt;code&gt;AppSecret&lt;/code&gt; / &lt;code&gt;Robot Name&lt;/code&gt; / &lt;code&gt;Robot Code&lt;/code&gt;)
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-dashboard-channels.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-secret.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-craete-application-robot-code.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;测试钉钉机器人&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开钉钉 APP, 直接在顶部的搜索框中搜索添加的 机器人名称
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-robot-search.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送消息给机器人
&lt;img src=&quot;./assets/images/openclaw-mac/dingtalk-robot-send.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://openclaw.ai/&quot; target=&quot;_blank&quot;&amp;gt;OpenClaw&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://o.doubao.com/&quot; target=&quot;_blank&quot;&amp;gt;豆包手机助手&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://nodejs.cn/&quot; target=&quot;_blank&quot;&amp;gt;Node.js&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys&quot; target=&quot;_blank&quot;&amp;gt;API Key - 智谱开放平台&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.openclaw.ai/zh-CN/tools/chrome-extension&quot; target=&quot;_blank&quot;&amp;gt;Chrome 扩展 - OpenClaw 文档&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://open.dingtalk.com/&quot; target=&quot;_blank&quot;&amp;gt;钉钉开放平台能力中心&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>在 Claude Code 中安装并使用 skills.sh 上的第三方技能(skills)</title><link>http://blog.xiaban.run/posts/2026/claude-code-skills-sh/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2026/claude-code-skills-sh/</guid><description>最近 Vercel 发布的一个 Skills 的分发平台 skills.sh, 上面有非常多的高质量的适用于特定领域的 Skills, 我们来尝试安装并使用这些 SKILLS</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 SKILLS&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://platform.claude.com/docs/zh-CN/agents-and-tools/agent-skills/overview&quot; target=&quot;_blank&quot;&amp;gt;Agent Skills&amp;lt;/a&amp;gt; 最初由 &lt;code&gt;Anthropic&lt;/code&gt; 提出, 简单来说, &amp;lt;a href=&quot;https://platform.claude.com/docs/zh-CN/agents-and-tools/agent-skills/overview&quot; target=&quot;_blank&quot;&amp;gt;Agent Skills&amp;lt;/a&amp;gt; 就是让你的 &lt;code&gt;Claude Code&lt;/code&gt; 的 &lt;strong&gt;拥有特定领域的专业知识或技能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如今 &lt;code&gt;LLM&lt;/code&gt; 就像一个通才, &lt;code&gt;LLM&lt;/code&gt; 已经在所有行业都能独当一面, 但我们在实际工程落地时, 往往需要的是 &lt;strong&gt;专业性&lt;/strong&gt;, 而不是 &lt;strong&gt;通用性&lt;/strong&gt;, 换句话说, 我们需要的是针对于我们的使用场景进行优化过的 &lt;code&gt;LLM&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
就像我们去医院看牙, 我们需要找的是专业的牙科医生, 而不是全科医生&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-skills-sh-dentistry.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://platform.claude.com/docs/zh-CN/agents-and-tools/agent-skills/overview&quot; target=&quot;_blank&quot;&amp;gt;Skills&amp;lt;/a&amp;gt; 恰好补齐了这个短板, 它允许你 &lt;strong&gt;对某个特定领域的 知识 / 技能 / 规则 进行封装, 让 &lt;code&gt;LLM&lt;/code&gt; 具有这一领域的专业性&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;为什么我们需要它&lt;/h3&gt;
&lt;p&gt;为什么 &lt;code&gt;LLM&lt;/code&gt; 在实际工程落地时总感觉 &lt;strong&gt;差点意思&lt;/strong&gt;? 很大程度上是因为 &lt;strong&gt;上下文的缺失&lt;/strong&gt; 和 &lt;strong&gt;工具的匮乏&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上下文&lt;/strong&gt;: &lt;code&gt;Claude Code&lt;/code&gt; 不知道你们团队的代码规范, 不知道你们的部署流程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具&lt;/strong&gt;: &lt;code&gt;Claude Code&lt;/code&gt; 默认只能读写文件, 运行终端命令, 并没有适用于特定场景的工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;SKILLS&lt;/code&gt; 完美地解决了这个问题, 它允许我们将 &lt;strong&gt;最佳实践(&lt;code&gt;Best Practices&lt;/code&gt;)&lt;/strong&gt; 固化下来. 以前我们需要在 Prompt 里反复强调 &quot;请使用 BEM 命名规范&quot;, 现在只需要安装一个 &lt;code&gt;css-best-practice&lt;/code&gt; 的 Skill, &lt;code&gt;Claude Code&lt;/code&gt; 就懂了&lt;/p&gt;
&lt;h2&gt;什么是 skills.sh&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://skills.sh&quot; target=&quot;_blank&quot;&amp;gt;skills.sh&amp;lt;/a&amp;gt; 是一个由 &lt;code&gt;vercel&lt;/code&gt; 开发的 &lt;strong&gt;&lt;code&gt;Skills&lt;/code&gt; 分发平台&lt;/strong&gt;, 它提供了 &lt;code&gt;skills CLI&lt;/code&gt; 工具来安装和查找 &lt;code&gt;SKILLS&lt;/code&gt;, 在官网也会列出所有流行的 &lt;code&gt;SKILLS&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://skills.sh&quot; target=&quot;_blank&quot;&amp;gt;skills.sh&amp;lt;/a&amp;gt; 的排名算法是根据 &lt;code&gt;skills add&lt;/code&gt; 命令的安装数量来排序的, 也就是说安装量越大, 排名越靠前, 在 &lt;code&gt;Skills&lt;/code&gt; 详情页也会显示不同的 &lt;code&gt;AI Coding Agent&lt;/code&gt; 工具的安装量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-skills-sh-details-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为目前最流行的 &lt;code&gt;SKILLS&lt;/code&gt; &amp;lt;a href=&quot;https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices&quot; target=&quot;_blank&quot;&amp;gt;vercel-react-best-practices&amp;lt;/a&amp;gt; 的详情页右侧部分内容, 可以看到安装次数最多的是 &lt;code&gt;Claude Code&lt;/code&gt;, 总共 &lt;code&gt;29.4k&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;是不是很像 &lt;code&gt;npmjs.com&lt;/code&gt; 中的详情页?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-skills-sh-npmjscom-details-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&amp;lt;a href=&quot;https://skills.sh&quot; target=&quot;_blank&quot;&amp;gt;skills.sh&amp;lt;/a&amp;gt; 做的正是成为 &lt;code&gt;Skills&lt;/code&gt; 界的 &lt;code&gt;npm&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;如何使用 skills.sh&lt;/h2&gt;
&lt;h3&gt;搜索 Skills&lt;/h3&gt;
&lt;p&gt;可以直接使用 &lt;code&gt;npx&lt;/code&gt; 调用 &lt;code&gt;skills&lt;/code&gt; 命令, 本质上是先下载 &lt;code&gt;skills CLI&lt;/code&gt; 到本地, 然后再执行 &lt;code&gt;skills&lt;/code&gt; 命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx skills find vue

Packages: +1
+
Progress: resolved 1, reused 0, downloaded 1, added 1, done

███████╗██╗  ██╗██╗██╗     ██╗     ███████╗
██╔════╝██║ ██╔╝██║██║     ██║     ██╔════╝
███████╗█████╔╝ ██║██║     ██║     ███████╗
╚════██║██╔═██╗ ██║██║     ██║     ╚════██║
███████║██║  ██╗██║███████╗███████╗███████║
╚══════╝╚═╝  ╚═╝╚═╝╚══════╝╚══════╝╚══════╝

Install with npx skills add &amp;lt;owner/repo@skill&amp;gt;

hyf0/vue-skills@vue-best-practices
└ https://skills.sh/hyf0/vue-skills/vue-best-practices

onmax/nuxt-skills@vue
└ https://skills.sh/onmax/nuxt-skills/vue

onmax/nuxt-skills@vueuse
└ https://skills.sh/onmax/nuxt-skills/vueuse

hyf0/vue-skills@pinia-best-practices
└ https://skills.sh/hyf0/vue-skills/pinia-best-practices

hyf0/vue-skills@vueuse-best-practices
└ https://skills.sh/hyf0/vue-skills/vueuse-best-practices

vueuse/skills@vueuse-functions
└ https://skills.sh/vueuse/skills/vueuse-functions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用 &lt;code&gt;pnpm dlx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm dlx skills find vue
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 Skills&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm dlx skills add hyf0/vue-skills@vue-best-practices

███████╗██╗  ██╗██╗██╗     ██╗     ███████╗
██╔════╝██║ ██╔╝██║██║     ██║     ██╔════╝
███████╗█████╔╝ ██║██║     ██║     ███████╗
╚════██║██╔═██╗ ██║██║     ██║     ╚════██║
███████║██║  ██╗██║███████╗███████╗███████║
╚══════╝╚═╝  ╚═╝╚═╝╚══════╝╚══════╝╚══════╝

┌   skills
│
◇  Source: https://github.com/hyf0/vue-skills.git @vue-best-practices
│
◇  Repository cloned
│
◇  Found 1 skill
│
●  Selected 1 skill: vue-best-practices
│
◇  Detected 4 agents
│
◆  Install to
│  ● All detected agents (Recommended) (Install to all 4 detected agents)
│  ○ Select specific agents
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 &lt;code&gt;skills CLI&lt;/code&gt; 会自动检测当前的 &lt;code&gt;AI Coding Agents&lt;/code&gt;, 这里检测到了 &lt;code&gt;4&lt;/code&gt; 个, 默认 &lt;code&gt;All detected agents (Recommended)&lt;/code&gt;, 也就是全部安装, 这里我们可以选择只安装到部分 &lt;code&gt;AI Coding Agents&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Select agents to install skills to (space to toggle)
│  ◻ Claude Code (.claude/skills)
│  ◻ GitHub Copilot
│  ◻ OpenCode
│  ◻ Trae
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们选择安装到 &lt;code&gt;Claude Code&lt;/code&gt;, 并且只安装到当前项目中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◇  Select agents to install skills to (space to toggle)
│  Claude Code
│
◆  Installation scope
│  ● Project (Install in current directory (committed with your project))
│  ○ Global
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们选择符号链接的形式, 这样会节省磁盘空间:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◆  Installation method
│  ● Symlink (Recommended) (Single source of truth, easy updates)
│  ○ Copy to all agents
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后会推荐我们安装官方的 &lt;code&gt;find-skills&lt;/code&gt;, 这里我们先不安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;◇  Installed 1 skill to 1 agent ───────────────────────────────────╮
│                                                                  │
│  ✓ ~/projects/blog.xiaban.run/.agents/skills/vue-best-practices  │
│    symlink → Claude Code                                         │
│                                                                  │
├──────────────────────────────────────────────────────────────────╯

│
└  Done!


│
│  One-time prompt - you won&apos;t be asked again if you dismiss.
│
◆  Install the find-skills skill? It helps your agent discover and suggest skills.
│  ○ Yes / ● No
└
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
安装位置选择, 以 &lt;code&gt;Claude Code&lt;/code&gt; 为例:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project (推荐)&lt;/strong&gt;: 安装到当前项目的 &lt;code&gt;.claude/skills&lt;/code&gt; 目录, 适合项目特定的规范 (如 &lt;code&gt;Lint&lt;/code&gt; 规则、代码风格)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global&lt;/strong&gt;: 安装到 &lt;code&gt;~/.claude/skills&lt;/code&gt; 目录, 适合通用的工具 (如 &lt;code&gt;git&lt;/code&gt; 操作 / &lt;code&gt;pdf&lt;/code&gt; 读取)
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 验证与更新 (Check &amp;amp; Update)&lt;/h3&gt;
&lt;p&gt;安装完成后, &lt;code&gt;Skills&lt;/code&gt; 会以文件夹的形式存在于你的 &lt;code&gt;.claude/skills&lt;/code&gt; (或 &lt;code&gt;~/.claude/skills&lt;/code&gt;) 目录下.&lt;/p&gt;
&lt;p&gt;你可以随时检查更新:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx skills check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者一键更新所有 Skills:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx skills update
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 实际使用 (Usage)&lt;/h3&gt;
&lt;p&gt;安装好 &lt;code&gt;react-best-practices&lt;/code&gt; 后, 当你在 &lt;code&gt;Claude Code&lt;/code&gt; 中让它写 React 组件时, 它会自动参考这些最佳实践.&lt;/p&gt;
&lt;p&gt;比如你可以直接说:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;请帮我重构这个组件, 优化一下性能&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 会自动加载 &lt;code&gt;react-best-practices&lt;/code&gt; 中的规则 (如使用 &lt;code&gt;Server Components&lt;/code&gt;, 优化 &lt;code&gt;useEffect&lt;/code&gt; 等) 来指导重构&lt;/p&gt;
&lt;h2&gt;对比 Prompts&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Skills&lt;/code&gt; 本质上就是 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Slash Commands&amp;lt;/a&amp;gt;, 相比于普通的 &lt;code&gt;prompts&lt;/code&gt;, 它有以下优势:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由 &lt;code&gt;AI Coding Agent&lt;/code&gt; 决定是否调用&lt;/li&gt;
&lt;li&gt;按需加载(默认只读取 &lt;code&gt;SKILLS.md&lt;/code&gt; 中的 meta 部分), 不会对性能造成影响&lt;/li&gt;
&lt;li&gt;可以包含(引用)资源和脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;趋势&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-skills-sh-diff.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于开源项目来说, 特别是成熟的工具库或框架, 肯定会有自己的文档, 如果让开发者来通过阅读文档来学习, 是非常耗时的, 现在有了 &lt;code&gt;Skills&lt;/code&gt;, 可以专门为这些项目编写 &lt;code&gt;Skills&lt;/code&gt;, 直接让 &lt;code&gt;AI Coding Agent&lt;/code&gt; 来加载 &lt;code&gt;skills&lt;/code&gt;(阅读文档), 直接省去了开发者自己去阅读文档的时间, 从这个角度来看, &lt;code&gt;Skills&lt;/code&gt; 是非常有价值的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
现在已经有非常多的开源项目推出了自己的 &lt;code&gt;Skills&lt;/code&gt;, 按照这个趋势, 未来 &lt;strong&gt;开发者就不需要阅读文档了, 而是直接通过加载 &lt;code&gt;Skills&lt;/code&gt; 的方式让 &lt;code&gt;AI Coding Agent&lt;/code&gt; 来加载技能&lt;/strong&gt;, 换句话说, 未来开发者只需要稍微了解一下某个领域, 就可以接入此领域的 &lt;code&gt;Skills&lt;/code&gt; 来完成任务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SKILLS&lt;/code&gt; 是将 &lt;strong&gt;&quot;隐性知识&quot;&lt;/strong&gt; (经验/规范) 转化为 &lt;strong&gt;&quot;显性能力&quot;&lt;/strong&gt; (工具/Prompt) 的最佳载体; 通过 &lt;code&gt;skills.sh&lt;/code&gt;, 我们可以像搭积木一样, 为我们的 &lt;code&gt;Claude&lt;/code&gt; 组装最强的大脑.&lt;/p&gt;
&lt;p&gt;强烈建议大家去 &amp;lt;a href=&quot;https://skills.sh&quot; target=&quot;_blank&quot;&amp;gt;skills.sh&amp;lt;/a&amp;gt; 逛逛, 把你的最佳实践也分享出来!&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.claude.com/docs/zh-CN/agents-and-tools/agent-skills/overview&quot;&gt;Agent Skills - Claude API Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://skills.sh/docs&quot;&gt;The Agent Skills Directory - skills.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://skills.sh/docs/cli&quot;&gt;skills.sh CLI Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7598807837868539930&quot;&gt;AI Skills：前端新的效率神器！&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/skills&quot;&gt;anthropics/skills - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://skills.sh&quot;&gt;skills.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices&quot;&gt;vercel-react-best-practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot;&gt;Slash Commands&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>OpenCode + GLM 安装和配置教程</title><link>http://blog.xiaban.run/posts/2026/opencode-glm/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2026/opencode-glm/</guid><description>OpenCode 是一个开源的 AI Coding Agent, 提供了 CLI / Client / IDE Extension, 可以接入绝大多数厂商的大模型, 并且提供了免费的模型, 相比于 Claude Code / Gemini Cli 等同类型的产品, 它更加开放</description><pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 是一个开源的 &lt;code&gt;AI Coding Agent&lt;/code&gt;, 提供了 &lt;code&gt;CLI&lt;/code&gt; / &lt;code&gt;Client&lt;/code&gt; / &lt;code&gt;IDE Extension&lt;/code&gt;, 并且可以使用任何模型, 相比于 &amp;lt;a href=&quot;https://claude.com/product/claude-code&quot; target=&quot;_blank&quot;&amp;gt;Claude Code&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://geminicli.com/&quot; target=&quot;_blank&quot;&amp;gt;Gemini Cli&amp;lt;/a&amp;gt; 等同类型的产品, 它更加开放&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 是一个开源免费的 &lt;code&gt;AI Coding Agent&lt;/code&gt;, 在各大厂商都发布自己的 &lt;code&gt;Coding Agent&lt;/code&gt; 时, &amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 完全开源和开放, 并且可以接入几乎市面上的所有厂商的 &lt;code&gt;API&lt;/code&gt;, 因为它的开放性, 它在 &lt;code&gt;github&lt;/code&gt; 上的 &lt;code&gt;stars&lt;/code&gt; 比 &amp;lt;a href=&quot;https://claude.com/product/claude-code&quot; target=&quot;_blank&quot;&amp;gt;Claude Code&amp;lt;/a&amp;gt; 还要多&lt;/p&gt;
&lt;h2&gt;割裂的开发标准&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;现阶段各大厂商都发布自己的 &lt;code&gt;AI Coding Agent&lt;/code&gt;&lt;/strong&gt;, 例如 &amp;lt;a href=&quot;https://claude.com/product/claude-code&quot; target=&quot;_blank&quot;&amp;gt;Claude Code&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://geminicli.com/&quot; target=&quot;_blank&quot;&amp;gt;Gemini Cli&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://trae.ai/&quot; target=&quot;_blank&quot;&amp;gt;trae&amp;lt;/a&amp;gt;, 但 &lt;strong&gt;每家都在制定自己的标准或协议&lt;/strong&gt;, 例如 &lt;code&gt;cluade code&lt;/code&gt; 使用 &lt;code&gt;.claude&lt;/code&gt;, &lt;code&gt;gemini cli&lt;/code&gt; 使用 &lt;code&gt;.gemini&lt;/code&gt;, &lt;code&gt;trae&lt;/code&gt; 使用 &lt;code&gt;.trae&lt;/code&gt;, &lt;strong&gt;对于 MCP / Command / Skills / Plugins 等的标准和支持度也有差异&lt;/strong&gt;, 也就是说如果你要使用某个厂商的模型编码, 那么就只能使用它们推出的 &lt;code&gt;AI Coding Agent&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 做的就是兼容各家厂商的大模型, 你可以在这里看到支持的 &amp;lt;a href=&quot;https://opencode.ai/docs/providers/&quot; target=&quot;_blank&quot;&amp;gt;providers&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 提供了多种安装方式, 这里我们选择使用 &amp;lt;a href=&quot;https://bun.com/&quot; target=&quot;_blank&quot;&amp;gt;bun&amp;lt;/a&amp;gt; 进行安装, 因为它比 Node.js 更快性能更好&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bun add -g opencode-ai

bun add v1.2.10 (db2e7d7f)

installed opencode-ai@1.1.25 with binaries:
 - opencode

2 packages installed [46.56s]

Blocked 1 postinstall. Run `bun pm -g untrusted` for details.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;其他安装方式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://opencode.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;brew install anomalyco/tap/opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pnpm i -g opencode-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;npm i -g opencode-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;choco install opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;免费模型&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 提供了免费的模型, 你可以在 &amp;lt;a href=&quot;https://opencode.ai/docs/zen/#pricing&quot; target=&quot;_blank&quot;&amp;gt;opencode zen pricing&amp;lt;/a&amp;gt; 中查看&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-free-models.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;配置 GLM Coding Plan&lt;/h2&gt;
&lt;p&gt;opencode zen 提供的免费模型好像有频率限制, 不是很稳定, 而且不支持 &amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/coding-plan/mcp/vision-mcp-server&quot; target=&quot;_blank&quot;&amp;gt;GLM 的 视觉理解等 MCP 服务&amp;lt;/a&amp;gt;, 这里可以直接接入已经订阅的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Plan&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;:::note[推广]
如果想要获得最好的 &lt;code&gt;Vibe Coding&lt;/code&gt; 体验, 推荐购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt; 服务(也可以 &amp;lt;a href=&quot;https://www.bigmodel.cn/activity/trial-card/A8AMOHCHA5&quot; target=&quot;_blank&quot;&amp;gt;🔗 点击这里&amp;lt;/a&amp;gt; 先免费试用 7 天), 包月只要 20 💰, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, 每 &lt;code&gt;5&lt;/code&gt; 小时最多约 &lt;code&gt;120&lt;/code&gt; 次 &lt;code&gt;prompts&lt;/code&gt;, 原因如下:
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm dlx @z_ai/coding-helper

╔═══════════════════════════════════════════════════════════════╗
║                                                               ║
║       ▄▀▀ ▄▀▄ █▀▄ █ █▄ █ ▄▀    █▄█ ██▀ █   █▀▄ ██▀ █▀▄        ║
║       ▀▄▄ ▀▄▀ █▄▀ █ █ ▀█ ▀▄█   █ █ █▄▄ █▄▄ █▀  █▄▄ █▀▄        ║
║                                                               ║
║                     Coding Helper v0.0.5                      ║
║        Manage Your Claude Code and Other Coding Tools         ║
╚═══════════════════════════════════════════════════════════════╝

╔═════════════════════════════════════════════════════════════╗
║                  Select interface language                  ║
╚═════════════════════════════════════════════════════════════╝

💡 ↑↓ Navigate | Enter Confirm

? ✨ Select interface language (Use arrow keys)
  [EN] English ✓ (Current)
❯ [CN] 中文
  ──────────────
  &amp;lt;-  Return
  x   Exit

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或使用 npx:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx @z_ai/coding-helper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改 &lt;code&gt;~/.config/opencode/opencode.json&lt;/code&gt; 中的模型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;$schema&quot;: &quot;https://opencode.ai/config.json&quot;,
    &quot;provider&quot;: {
        &quot;zhipuai-coding-plan&quot;: {
            &quot;options&quot;: {
                &quot;apiKey&quot;: &quot;04eb870cce874c3d83c4694f0c35cc43.0I6oTtTbC7Pg4nOY&quot;
            }
        }
    },
    &quot;model&quot;: &quot;zhipuai-coding-plan/glm-4.7&quot;,
    &quot;small_model&quot;: &quot;zhipuai-coding-plan/glm-4.7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
在发布新模型时需要再次修改这里的 &lt;code&gt;model&lt;/code&gt; / &lt;code&gt;small_model&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;接入其他模型&lt;/h2&gt;
&lt;p&gt;其他供应商或模型的接入方式可以查看 &amp;lt;a href=&quot;https://opencode.ai/docs/providers/&quot; target=&quot;_blank&quot;&amp;gt;providers&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;启动&lt;/h2&gt;
&lt;p&gt;最终启动 &lt;code&gt;opencode&lt;/code&gt; 时可能会遇到没有权限创建目录的问题:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opencode

EACCES: permission denied, mkdir &apos;/Users/xxx/.local/state/opencode&apos;
    path: &quot;/Users/xxx/.local/state/opencode&quot;,
 syscall: &quot;mkdir&quot;,
   errno: -13,
    code: &quot;EACCES&quot;


Bun v1.3.5 (macOS arm64)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来只是单纯的目录权限问题, 我们来看看当前用户是否有权限:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsd -l ~/.local/

drwxr-xr-x xxx staff 320 B Wed Nov 12 15:06:56 2025  bin
drwxr-xr-x xxx staff  96 B Fri Nov  7 14:16:36 2025  lib
drwx------ xxx staff 256 B Mon Jan 19 13:24:03 2026  share
drwxr-xr-x root  staff  96 B Wed Feb 21 13:57:59 2024  state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;~/.local/state&lt;/code&gt; 目录属于 &lt;code&gt;root&lt;/code&gt; 用户, 我们将其改为当前用户:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chown -R $USER ~/.local/state
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动 opencode&lt;/h2&gt;
&lt;p&gt;直接执行 &lt;code&gt;opencode [path]&lt;/code&gt; 启动 &lt;code&gt;opencode&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opencode

                                                                                 ▄
                                                █▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
                                                █  █ █  █ █▀▀▀ █  █ █    █  █ █  █ █▀▀▀
                                                ▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀


                              ┃
                              ┃  Ask anything... &quot;Fix broken tests&quot;
                              ┃
                              ┃  Build  GLM-4.6 Zhipu AI Coding Plan
                              ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                                             ctrl+t variants  tab agents  ctrl+p commands

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-shotcut.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;h3&gt;引用文件&lt;/h3&gt;
&lt;p&gt;直接输入 &lt;code&gt;@&lt;/code&gt; 符号就可以在输入框中搜索并引用文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-link-file.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;执行命令&lt;/h3&gt;
&lt;p&gt;直接输入 &lt;code&gt;!&lt;/code&gt; 符号, 然后输入命令按下回车即可执行, 例如 &lt;code&gt;! ll -la&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-command1.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/opencode-glm-command2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Editor&lt;/h3&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 支持打开外部编辑器编写信息, 在编辑长信息时可能有点用, 首先添加环境变量 &lt;code&gt;EDITOR&lt;/code&gt;, 然后输入 &lt;code&gt;/editor&lt;/code&gt; 即可进入 &lt;code&gt;$EDITOR&lt;/code&gt; 制定的编辑器中编辑信息, 具体配置方式参考 &amp;lt;a href=&quot;https://opencode.ai/docs/tui/#editor-setup&quot; target=&quot;_blank&quot;&amp;gt;editor setup - OpenCode&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;例如我们将外部编辑器设置为 &lt;code&gt;vim&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Editor
export EDITOR=vim

# for vscode
# export EDITOR=&quot;code --wait&quot;

# for trae
# export EDITOR=&quot;trae --wait&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Editor
set -gx EDITOR vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 &lt;code&gt;source&lt;/code&gt; 加载一下相关的配置文件, 再启动 &lt;code&gt;opencode&lt;/code&gt; 即可&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;code&gt;opencode&lt;/code&gt; 并不像 &lt;code&gt;claude code&lt;/code&gt; 一样可以在输入消息时设置为使用 &lt;code&gt;vi&lt;/code&gt; / &lt;code&gt;vim&lt;/code&gt; 键绑定, 作为 &lt;code&gt;vim&lt;/code&gt; 用户非常不爽&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;初始化项目&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;/init&lt;/code&gt; 命令生成被大多数 &lt;code&gt;AI Coding Agent&lt;/code&gt; 支持的 &lt;code&gt;AGENTS.md&lt;/code&gt; 文件:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-init-command.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;执行后 &lt;code&gt;LLM&lt;/code&gt; 会阅读整个项目的源码, 然后生成一个让 &lt;code&gt;LLM&lt;/code&gt; 可以快速理解项目的 &lt;code&gt;markdown&lt;/code&gt; 文件, 并且每次启动 &lt;code&gt;opencode&lt;/code&gt; 时都会加载这个 &lt;code&gt;AGENTS.md&lt;/code&gt; 文件, 类似于 &lt;code&gt;Claude Code&lt;/code&gt; 的 &lt;code&gt;CLAUDE.md&lt;/code&gt; 文件, 但是他更加通用&lt;/p&gt;
&lt;h3&gt;切换模型&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;/models&lt;/code&gt; 查看现有的模型:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-models-command.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我们切换为 &lt;code&gt;glm-4.7&lt;/code&gt;, 然后确定&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-models-after-command.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;thinking&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;/thinking&lt;/code&gt; 切换推理/思考过程是否显示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-thinking-command.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;直接执行&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;opencode run &quot;...&quot;&lt;/code&gt; 可以不打开交互式终端直接发送消息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opencode run &quot;你好&quot;


你好！我是 opencode，一个可以帮助你进行软件工程任务的 CLI 工具。我可以帮助你：

- 编写和修改代码
- 分析代码库结构
- 运行构建和测试命令
- 解决 bug 和添加新功能
- 代码审查和重构

需要我帮助你做什么吗？
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看统计数据&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;opencode stats&lt;/code&gt; 查看 token 消耗量 / 消费额 等信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opencode stats
┌────────────────────────────────────────────────────────┐
│                       OVERVIEW                         │
├────────────────────────────────────────────────────────┤
│Sessions                                              3 │
│Messages                                             29 │
│Days                                                  1 │
└────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│                    COST &amp;amp; TOKENS                       │
├────────────────────────────────────────────────────────┤
│Total Cost                                        $0.00 │
│Avg Cost/Day                                      $0.00 │
│Avg Tokens/Session                                49.8K │
│Median Tokens/Session                             11.6K │
│Input                                            147.3K │
│Output                                             2.1K │
│Cache Read                                       123.2K │
│Cache Write                                           0 │
└────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────┐
│                      TOOL USAGE                        │
├────────────────────────────────────────────────────────┤
│ read               ████████████████████   6 (42.9%)    │
│ glob               █████████████          4 (28.6%)    │
│ bash               ██████████             3 (21.4%)    │
│ write              ███                    1 ( 7.1%)    │
└────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;web 模式&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;opencode web&lt;/code&gt; 会启动一个 &lt;code&gt;web&lt;/code&gt; 服务, 可以在浏览器中通过 web 页面的形式使用 &lt;code&gt;opencode&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-web-mode.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;mdns discovery&lt;/h4&gt;
&lt;p&gt;执行 &lt;code&gt;opencode web --mdns&lt;/code&gt; 可以使此 &lt;code&gt;web&lt;/code&gt; 服务在本地网络中被公开访问, 参考 &amp;lt;a href=&quot;https://opencode.ai/docs/web/#mdns-discovery&quot; target=&quot;_blank&quot;&amp;gt;mdns discovery&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;vscode extension&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;opencode&lt;/code&gt; 提供了 &lt;code&gt;vscode extension&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/opencode-glm-vscode-extension.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;更新&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;opencode upgrade

# 制定更新方式, 支持 curl / npm / pnpm / bun / brew
opencode upgrade --method bun
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;指令&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://opencode.ai&quot; target=&quot;_blank&quot;&amp;gt;OpenCode&amp;lt;/a&amp;gt; 自带了一些常用的指令:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/compact&lt;/code&gt;: 压缩会话&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/clear&lt;/code&gt;: 清空会话&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/export&lt;/code&gt;: 导出会话, 导出为 &lt;code&gt;markdown&lt;/code&gt; 文件, 并使用 &lt;code&gt;$EDITOR&lt;/code&gt; 打开文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/init&lt;/code&gt;: 初始化 &lt;code&gt;AGENTS.md&lt;/code&gt; 文件, 详见 &lt;a href=&quot;#%E5%88%9D%E5%A7%8B%E5%8C%96%E9%A1%B9%E7%9B%AE&quot;&gt;初始化项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/models&lt;/code&gt;: 列出支持的 &lt;code&gt;LLM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/session&lt;/code&gt;: 查看所有历史会话&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/shart&lt;/code&gt;: 分享当前会话, 类似于 &lt;code&gt;ChatGPT&lt;/code&gt; 的 &lt;code&gt;Share&lt;/code&gt; 功能&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;快捷键&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ctrl+c&lt;/code&gt;: 退出 &lt;code&gt;opencode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Esc&lt;/code&gt; / &lt;code&gt;ctrl+[&lt;/code&gt;: 退出当前模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tab&lt;/code&gt;: 切换模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我更习惯使用 &lt;code&gt;command&lt;/code&gt;, 所以真正用到的快捷键只有这些&lt;/p&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当前项目的配置文件在 &lt;code&gt;.opencode.jsonc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;全局配置文件在 &lt;code&gt;~/.config/opencode/opencode.jsonc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MCP&lt;/h2&gt;
&lt;p&gt;直接在配置文件中声明 MCP 服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://opencode.ai/config.json&quot;,
  &quot;mcp&quot;: {
    &quot;my-remote-mcp&quot;: {
      &quot;type&quot;: &quot;remote&quot;,
      &quot;url&quot;: &quot;https://my-mcp-server.com&quot;,
      &quot;enabled&quot;: true,
      &quot;headers&quot;: {
        &quot;Authorization&quot;: &quot;Bearer MY_API_KEY&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Skills&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;opencode&lt;/code&gt; 会从以下位置读取 &lt;code&gt;skills&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目配置: &lt;code&gt;.opencode/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;全局配置: &lt;code&gt;~/.config/opencode/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;项目 Claude Code: &lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;全局 Claude Code: &lt;code&gt;~/.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SKILL.md 文件示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
name: git-release
description: Create consistent releases and changelogs
license: MIT
compatibility: opencode
metadata:
  audience: maintainers
  workflow: github
---

## What I do

- Draft release notes from merged PRs
- Propose a version bump
- Provide a copy-pasteable `gh release create` command

## When to use me

Use this when you are preparing a tagged release.
Ask clarifying questions if the target versioning scheme is unclear.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义工具&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;opencode&lt;/code&gt; 会读取 &lt;code&gt;.opencode/tools/*&lt;/code&gt; 作为可以直接调用的工具, 以 &lt;code&gt;Nodejs&lt;/code&gt; 为例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { tool } from &quot;@opencode-ai/plugin&quot;

export const add = tool({
  description: &quot;Add two numbers&quot;,
  args: {
    a: tool.schema.number().describe(&quot;First number&quot;),
    b: tool.schema.number().describe(&quot;Second number&quot;),
  },
  async execute(args) {
    return args.a + args.b
  },
})

export const multiply = tool({
  description: &quot;Multiply two numbers&quot;,
  args: {
    a: tool.schema.number().describe(&quot;First number&quot;),
    b: tool.schema.number().describe(&quot;Second number&quot;),
  },
  async execute(args) {
    return args.a * args.b
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就创建了两个工具: &lt;code&gt;math_add&lt;/code&gt; 和 &lt;code&gt;math_multiply&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;权限&lt;/h2&gt;
&lt;p&gt;可以在项目中创建 &lt;code&gt;opencode.jsonc&lt;/code&gt; 文件(&lt;code&gt;.jsonc&lt;/code&gt; 文件允许添加注释) 来配置 &lt;code&gt;opencode&lt;/code&gt;, 最重要的就是权限相关的配置了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://opencode.ai/config.json&quot;,
  &quot;permission&quot;: {
    &quot;edit&quot;: &quot;deny&quot;, // 拒绝修改
    &quot;bash&quot;: &quot;ask&quot;, // 执行 bash 命令之前询问
    &quot;webfetch&quot;: &quot;allow&quot;, // 允许执行 webfetch
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用更加细粒度的权限控制:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://opencode.ai/config.json&quot;,
  &quot;permission&quot;: {
    &quot;bash&quot;: {
      &quot;*&quot;: &quot;ask&quot;,
      &quot;git *&quot;: &quot;allow&quot;,
      &quot;npm *&quot;: &quot;allow&quot;,
      &quot;rm *&quot;: &quot;deny&quot;,
      &quot;grep *&quot;: &quot;allow&quot;
    },
    &quot;edit&quot;: {
      &quot;*&quot;: &quot;deny&quot;,
      &quot;packages/web/src/content/docs/*.mdx&quot;: &quot;allow&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
全局配置文件在 &lt;code&gt;~/.config/opencode/opencode.jsonc&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;可配置权限&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;OpenCode&lt;/code&gt; 的权限是根据工具名称来划分的, 此外还有一些安全措施:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;read: 读取文件（匹配文件路径）&lt;/li&gt;
&lt;li&gt;edit: 所有文件修改（涵盖edit，，，）writepatchmultiedit&lt;/li&gt;
&lt;li&gt;glob: 文件通配符（匹配通配符模式）&lt;/li&gt;
&lt;li&gt;grep: 内容搜索（匹配正则表达式模式）&lt;/li&gt;
&lt;li&gt;list: 列出目录中的文件（与目录路径匹配）&lt;/li&gt;
&lt;li&gt;bash: 运行 shell 命令（匹配已解析的命令，例如git status --porcelain）&lt;/li&gt;
&lt;li&gt;task: 启动子代理（与子代理类型匹配）&lt;/li&gt;
&lt;li&gt;skill: 加载技能（与技能名称匹配）&lt;/li&gt;
&lt;li&gt;lsp: 运行 LSP 查询（目前是非细粒度的）&lt;/li&gt;
&lt;li&gt;todoread / todowrite: 阅读/更新待办事项列表&lt;/li&gt;
&lt;li&gt;webfetch: 获取 URL（与 URL 匹配）&lt;/li&gt;
&lt;li&gt;websearch / codesearch: 网络/代码搜索（与查询匹配）&lt;/li&gt;
&lt;li&gt;external_directory: 当工具访问项目工作目录之外的路径时触发&lt;/li&gt;
&lt;li&gt;doom_loop: 当同一工具调用重复 3 次且输入完全相同时触发&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自定义命令&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;opencode&lt;/code&gt; 的自定义命令跟 &lt;code&gt;Claude Code&lt;/code&gt; 的 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Slash Commands&amp;lt;/a&amp;gt; 基本一致, 可以直接参考我的另一篇文章 &amp;lt;a href=&quot;../../2025/claude-code-slash-commands/&quot; target=&quot;_blank&quot;&amp;gt;Claude Code 系列教程之 斜杠命令(Slash Commands)&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
description: Create a new component
---

Create a new React component named $ARGUMENTS with TypeScript support.
Include proper typing and basic structure.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不同点在于参数的声明和使用, 在 &lt;code&gt;opencode&lt;/code&gt; 中直接使用 &lt;code&gt;$ARGUMENTS&lt;/code&gt; 或者 &lt;code&gt;$1&lt;/code&gt; / &lt;code&gt;$2&lt;/code&gt; 来使用参数, 不支持声明参数&lt;/p&gt;
&lt;p&gt;更多使用方式参考 &amp;lt;a href=&quot;https://opencode.ai/docs/commands&quot; target=&quot;_blank&quot;&amp;gt;opencode commands&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;formatter&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;opencode&lt;/code&gt; 支持在更新完代码之后自动进行格式化, 例如 &lt;code&gt;golang&lt;/code&gt; 使用 &lt;code&gt;gofmt&lt;/code&gt;, &lt;code&gt;rust&lt;/code&gt; 使用 &lt;code&gt;rustfmt&lt;/code&gt;, 前端项目使用 &lt;code&gt;prettier&lt;/code&gt; / &lt;code&gt;biome&lt;/code&gt; / &lt;code&gt;oxfmt&lt;/code&gt; ... 更多详情参考 &amp;lt;a href=&quot;https://opencode.ai/docs/formatters&quot; target=&quot;_blank&quot;&amp;gt;formatters&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai&quot;&gt;OpenCode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.com/product/claude-code&quot;&gt;Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://geminicli.com/&quot;&gt;Gemini Cli&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://trae.ai/&quot;&gt;trae&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.com/&quot;&gt;bun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.bigmodel.cn/cn/coding-plan/mcp/vision-mcp-server&quot;&gt;GLM 的 视觉理解等 MCP 服务&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot;&gt;GLM Coding Plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bigmodel.cn/activity/trial-card/A8AMOHCHA5&quot;&gt;点击这里&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs/tui/#editor-setup&quot;&gt;editor setup - OpenCode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot;&gt;Slash Commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../../2025/claude-code-slash-commands/&quot;&gt;Claude Code 系列教程之 斜杠命令(Slash Commands)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs/zen/#pricing&quot;&gt;opencode zen pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs/providers/&quot;&gt;providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs/commands&quot;&gt;opencode commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs/formatters&quot;&gt;formatters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs/web/#mdns-discovery&quot;&gt;mdns discovery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>AI 互动剧情类游戏生成工具开发记录2 - 构建工作流</title><link>http://blog.xiaban.run/posts/2026/movie-games-optimize/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2026/movie-games-optimize/</guid><description>在上一篇文章中, 介绍了一下我要做的互动剧情类游戏生成器, 然后分析了目前面临的严重影响使用的问题, 现在我们来通过构建工作流的方式来解决这些问题</description><pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 &amp;lt;a href=&quot;/movie-games-record&quot; target=&quot;_blank&quot;&amp;gt;上一篇文章&amp;lt;/a&amp;gt; 中, 介绍了一下我要做的互动剧情类游戏生成器, 然后分析了目前面临的严重影响使用的问题, 现在我们来通过构建工作流的方式来解决这些问题&lt;/p&gt;
&lt;h2&gt;失败的设计&lt;/h2&gt;
&lt;p&gt;在现有的设计方案中, &lt;strong&gt;我把一切都交给了 LLM&lt;/strong&gt;, 并且在 &lt;code&gt;propmpt&lt;/code&gt; 中精心编写了各种限制, 我本以为它会生成符合所有要求的 &lt;code&gt;JSON&lt;/code&gt;, 但是总是出现各种问题&lt;/p&gt;
&lt;p&gt;倘若这是一个没有与 LLM 交互的项目, 那总会找到解决方案, 因为 &lt;strong&gt;程序输出的结果是确定的, 而 LLM 输出的结构是不确定的&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;超长的上下文&lt;/h3&gt;
&lt;p&gt;现有的提示词(&lt;strong&gt;已省略部分内容&lt;/strong&gt;)如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 角色定义
你是一位享誉全球的互动电影游戏编剧和总导演。你擅长创作引人入胜、逻辑严密且充满情感冲击力的多分支剧情。 你的任务是根据用户提供的主题，创作一个完整的互动电影剧本，并将其直接输出为符合 TypeScript 接口定义的 JSON 格式。

# 用户输入主题
{}
# 一、核心叙事与风格要求
- 第一人称沉浸式叙事：所有的 `node.content` 必须使用 **第一人称 (&quot;我&quot;)** 进行叙述。玩家就是主角，代入感必须极强。
- 剧情深度与质量：
    - 拒绝流水账、拒绝平铺直叙、拒绝假大空。
    - 必须具备电影剧本般的 **真实感、细腻度与情感张力**。
    - 严禁任何无意义的故事情节或重复啰嗦的废话。
- 语言指定：所有剧情内容必须使用 **{}** 撰写。

# 二、基础结构与格式约束
- JSON 结构规范：
  - 严格遵循下方的 `TypeScript` 类型定义。
  - 禁止返回 `meta` / `projectId` / `nodes[].id` / `version` / `owner` / `provenance` 等字段。
  - 结局分离：所有的结局节点必须定义在顶层的 `endings` 字段中。
  - ID 格式：
    - `nodes` 的 Key 必须是 **纯数字字符串** (例如 &quot;1&quot;, &quot;2&quot;, &quot;3&quot;...)。
    - **绝对禁止** 使用 `n_` 前缀 (如 `n_1`, `n_start` 等都是错误的)。
    - 唯一例外：起始节点的 Key 必须固定为 **&quot;start&quot;**。
  - 结局引用：`StoryNode` 中的 `choices` 若指向结局，必须引用 `endings` 中的 key。

# 三、数值硬性约束 (校验失败将视为错误)
- 节点总数：`nodes` 的数量必须在 **45 到 85** 之间。
- 结局数量：`endings` 的数量必须在 **4 到 6** 之间。
- 单节点字数：每个节点的 `content` (AI 智能扩写) 字数必须严格控制在 **45 到 65 字** 之间。
- 节点内容：每个节点的 `content` 禁止出现任何中文或者英文的双引号, 双引号必须使用单引号代替
- 路径深度：必须保证绝大多数的故事线都经过 **至少 12 个节点**。

...
# 六、结局触发机制
- 灵活结局：`endings` 的 Key 不再固定，可以根据剧情自由命名 (如 `ending_hero`, `ending_regret` 等)。
- 结局描述：每个结局的 `description` 长度不能超过 **40 个字**。
- 快速通道：**必须包含一个可以快速到达的结局路径**。
  - 例如：从 Start -&amp;gt; 节点 3 -&amp;gt; 节点 5 -&amp;gt; (选择某选项) -&amp;gt; 直接到达结局。
  - 也就是说，在较早的层级 (如 Level 3-5) 就允许通过特定选项直接进入结局。
- 互斥规则：
  - `nodes` 中的节点 **不允许** 包含 `endingKey` 属性。
  - 结局只能通过 `choices.nextNodeId` 指向 `endings` 的 Key 来触发。

# 用户提供的角色清单 (JSON)
{}
# TypeScript 类型定义 (Schema)
typescript {}
# 输出规则
- 输出必须是 **纯 JSON** 文本。
- **不要** 包含 markdown 代码块标记。
- `nodes` 数量：**45~85**。
- `endings` 数量：**3~6**。
- 必须包含 `start` 节点。
- 禁止出现任何游离节点(从 start 节点开始到达不了的节点)
- **每层节点数量不能超过 5 个, 在每次输出节点时都要检查是否超过这个数量值**
- **必须从最开始就要规划节点数量, 任何节点的选项引用的节点 id 都不允许超过这个数量值**
开始创作！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 &lt;code&gt;{}&lt;/code&gt; 是用户的输入(标题 / 简介 / 角色列表) 还有 &lt;code&gt;ts&lt;/code&gt; 类型定义&lt;/p&gt;
&lt;p&gt;此时 LLM 同时承担了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🎨 高质量文学创作（电影级叙事）&lt;/li&gt;
&lt;li&gt;🧠 全局规划（45–85 节点、12–15 层、选项比例、收束率）&lt;/li&gt;
&lt;li&gt;🧮 严格数学约束满足（DAG、ID 递增、层级宽度、百分比）&lt;/li&gt;
&lt;li&gt;🧾 精确 JSON Schema 输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终造成了 LLM 在 &lt;strong&gt;长上下文生成&lt;/strong&gt; 时出现了 遗忘 / 幻觉 / 格式约束失败 的问题&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
出现问题的原因并不是因为 &lt;code&gt;prompt&lt;/code&gt; 写的不够详细, 而是因为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;超长的内容已经超过了 &lt;code&gt;LLM&lt;/code&gt; 输出稳定性的边界&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;LLM&lt;/code&gt; 并不会对剧情内容进行反复打磨, 而是一次性生成&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束越多, 输出越趋同和保守&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型并不会 记住整部故事，它只是在概率上 看过&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;剧情树设计存在的问题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;assets/images/movie-games-optimize-mow.excalidraw.svg&quot; alt=&quot;现有的剧情树设计&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以上是现有的剧情树设计, 上方是剧情树中的节点, 下方是结局节点, 从 &lt;code&gt;Start Node&lt;/code&gt; 节点开始, 每个节点都指向下一个层级(&lt;code&gt;level&lt;/code&gt;)的某个节点(或者结局节点), 最终到达 &lt;code&gt;Endings&lt;/code&gt; 节点结束, 通过设计图很容易发现一些问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成了 &lt;code&gt;21&lt;/code&gt; 个节点, 最终玩家游玩一次只经过 &lt;code&gt;3 ~ 7&lt;/code&gt; 个节点, 实际节点利用率只有 &lt;code&gt;14%&lt;/code&gt; ~ &lt;code&gt;33%&lt;/code&gt;, &lt;strong&gt;大部分节点是玩家必须多次游玩才能到达, 但实际上很少会有玩家会玩第二次&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;上一篇文章介绍的 &amp;lt;a href=&quot;../movie-games-record/#剧情状态丢失&quot; target=&quot;_blank&quot;&amp;gt;剧情状态丢失&amp;lt;/a&amp;gt; 问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Layered Directed Acyclic Graph&lt;/h3&gt;
&lt;p&gt;在上图中我们发现这已经不是一个树结构了, 我们需要一个准确的结构定义, 那就是 &lt;strong&gt;分层有向无环图 (&lt;code&gt;Layered Directed Acyclic Graph&lt;/code&gt;)&lt;/strong&gt;, 并在此基础上增加了额外的数学约束:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;层级单调约束 (Strict Monotonicity)：对于任意边 $(u, v)$，必须满足 $Level(v) &amp;gt; Level(u)$。严禁 $Level(v) \le Level(u)$&lt;/li&gt;
&lt;li&gt;宽度约束 (Layer Breadth)：对于任意 $i \in [1, 25]$，属于层级 $L_i$ 的节点数 $|V_i| \le 3$&lt;/li&gt;
&lt;li&gt;连通性约束 (Reachability)：从起始节点 start 出发，通过有向边必须能够到达至少一个结局节点（Leaf Node），且图中不存在入度为 0 的非起始节点（游离节点）&lt;/li&gt;
&lt;li&gt;状态原子性 (State Atomicity)：逻辑门判定（Check）所使用的 flag 必须预先定义在全局状态池中，禁止生成未定义的状态位&lt;/li&gt;
&lt;li&gt;收束汇聚 (Convergence)：允许多个不同节点的 Link 指向同一个后继节点 $v$&lt;/li&gt;
&lt;li&gt;在收束节点 $v$，其剧情文本必须具备通用性，或通过 CONDITIONAL 逻辑门对不同路径的状态进行适配。&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-optimize-three.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我把问题分别发送给了 &lt;code&gt;ChatGPT&lt;/code&gt; / &lt;code&gt;Gemini&lt;/code&gt; / &lt;code&gt;DeepSeek&lt;/code&gt;, 有趣的是, 它们的给出的方案既有重叠的部分, 也有自己独到的一面&lt;/p&gt;
&lt;p&gt;为了解决在长上下文场景下大模型输出的不确定性问题, 我们可以将 prompt 拆解为多个 promot, 这样做就能做到:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 prompt 只完成一件事&lt;/li&gt;
&lt;li&gt;减少每个 prompt 的长度&lt;/li&gt;
&lt;li&gt;缩短整个剧情生成的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;共同点&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ChatGPT&lt;/code&gt; / &lt;code&gt;Gemini&lt;/code&gt; / &lt;code&gt;DeepSeek&lt;/code&gt; 给出的方案共同之处在于:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构生成 与 内容生成 分离&lt;/li&gt;
&lt;li&gt;剧情节点并发生成&lt;/li&gt;
&lt;li&gt;引入剧情状态配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不同之处在于:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;剧情状态设计&lt;/li&gt;
&lt;li&gt;剧情节点的并发生成方式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以点击查看所有模型的对话:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chatgpt.com/share/6964a414-ade4-800b-b839-5b53fd8a1ce4&quot; target=&quot;_blank&quot;&amp;gt;ChatGPT&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://gemini.google.com/share/3359fe7f9880&quot; target=&quot;_blank&quot;&amp;gt;Gemini&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chat.deepseek.com/share/pysplefy24u9rk71gc&quot; target=&quot;_blank&quot;&amp;gt;DeepSeek&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ChatGPT 激进的拆解派&lt;/h3&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://chatgpt.com/share/6964a414-ade4-800b-b839-5b53fd8a1ce4&quot; target=&quot;_blank&quot;&amp;gt;点击这里&amp;lt;/a&amp;gt; 查看完整对话&lt;/p&gt;
&lt;h4&gt;Prompt A 节点规划&lt;/h4&gt;
&lt;p&gt;这个 Prompt 只做一件事: &lt;strong&gt;不关注具体的剧情内容, 只生成剧情数的结构&lt;/strong&gt;, 具体职责为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确定 &lt;code&gt;Level&lt;/code&gt; 层数&lt;/li&gt;
&lt;li&gt;每个 &lt;code&gt;Level&lt;/code&gt; 的节点数&lt;/li&gt;
&lt;li&gt;哪些 &lt;code&gt;Level&lt;/code&gt; 是收束点&lt;/li&gt;
&lt;li&gt;哪些节点是复用节点&lt;/li&gt;
&lt;li&gt;哪些节点允许直达结局&lt;/li&gt;
&lt;li&gt;每个节点的 &lt;strong&gt;简短摘要&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Prompt B 生成起始节点和结局节点&lt;/h4&gt;
&lt;p&gt;由于起始节点和结局节点的特殊性, 需要先进行生成&lt;/p&gt;
&lt;h4&gt;Prompt C 好感度数据&lt;/h4&gt;
&lt;p&gt;好感度系统是基于角色的 &lt;strong&gt;剧情状态&lt;/strong&gt; 设计方式, 在众多游戏中, 都或多或少的存在好感度系统, 例如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;玩家选择了某个支线任务, 与某个角色共同完成了某段剧情, 在后续的主线中就会再次遇到这个角色, 而如果不做这个支线任务, 就不会遇到他&lt;/li&gt;
&lt;li&gt;需要做出一系列偏向与某个角色的选择, 后续才能进入此角色的故事线&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Prompt D 节点内容生成&lt;/h4&gt;
&lt;p&gt;这个 Prompt 只做一件事: &lt;strong&gt;生成单个节点内容&lt;/strong&gt;, 具体实现为:&lt;/p&gt;
&lt;p&gt;在 prompt 中加入 &lt;code&gt;Prompt A&lt;/code&gt; 生成的某个节点的信息(&lt;strong&gt;只包含此节点的信息, 不包含其他节点的信息&lt;/strong&gt;), 包含:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;故事的基本信息&lt;/li&gt;
&lt;li&gt;有几个选项&lt;/li&gt;
&lt;li&gt;起始节点和结局节点&lt;/li&gt;
&lt;li&gt;节点简短摘要&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;本地化数据校验&lt;/h4&gt;
&lt;p&gt;生成完节点树数据后, 通过程序校验数据&lt;/p&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ChatGPT&lt;/code&gt; 使用了先生成(包含节点 &lt;strong&gt;简短摘要&lt;/strong&gt; 的)节点树结构, 然后再每个节点单独生成具体的内容和选项的方式, 这种方式虽然对长上下文进行了合理的拆解, 但考虑的过于简单&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在生成剧情树结构时生成 简短摘要, &lt;strong&gt;依然存在 长上下文场景下 LLM 输出不稳定的问题&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每个节点单独生成时, LLM 会忽视大量的细节, 而且完全 &lt;strong&gt;没有解决 剧情状态 丢失的问题&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地化校验 在本项目中不应该存在&lt;/strong&gt;, 因为一旦校验失败就会重新生成, 耗时进一步增加&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此这个方案是失败的&lt;/p&gt;
&lt;h3&gt;DeepSeek 分层生成策略&lt;/h3&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://chat.deepseek.com/share/pysplefy24u9rk71gc&quot; target=&quot;_blank&quot;&amp;gt;点击这里&amp;lt;/a&amp;gt; 查看完整对话&lt;/p&gt;
&lt;p&gt;DeepSeek 的方案基本与 ChatGPT 方案一致, 唯一的不同点在于: 使用了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生成故事大纲, 而非节点树结构&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层生成策略&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;故事大纲&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;fn phase1_prompt(theme: &amp;amp;str, characters: &amp;amp;[Character]) -&amp;gt; String {
    format!(
        r#&quot;作为顶级互动电影编剧，请设计一个故事大纲：

主题：{}
角色：{}

请设计：
1. 核心冲突（50字以内）
2. 4-6个可能的结局方向（每个1句话）
3. 关键剧情转折点（3-5个）
4. 角色关系图谱

返回JSON格式：{{
    &quot;conflict&quot;: &quot;核心冲突描述&quot;,
    &quot;endings&quot;: [&quot;结局1&quot;, &quot;结局2&quot;...],
    &quot;turning_points&quot;: [&quot;转折1&quot;, &quot;转折2&quot;...],
    &quot;character_relations&quot;: {{&quot;角色1&quot;: [&quot;与角色2的关系&quot;, ...]}}
}}
&quot;#,
        theme, characters_json
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从返回的 JSON 数据结构来看, 故事大纲并没有解决节点树的数据结构上存在的任何问题, 返回增加了不确定性&lt;/p&gt;
&lt;h4&gt;分层生成策略&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;fn phase2_prompt(
    layer: usize,
    previous_layer: &amp;amp;[StoryNode],
    story_state: &amp;amp;StoryState,
    outline: &amp;amp;Outline,
) -&amp;gt; String {
    format!(
        r#&quot;## 上下文
当前生成第{}层节点，前一层的节点：
{}

当前剧情状态：
- 角色好感度：{}
- 已触发的关键事件：{}

## 任务
生成本层（第{}层）的节点：
- 本层最多生成{}个节点（根据剧情复杂度决定）
- 每个节点必须继承前一层的剧情状态
- 节点ID范围：{}-{}
- 设计时考虑：{}
  * 剧情收束点（多个前驱指向同一个节点）
  * 分支扩展点（创造新的分支）
  * 快速结局路径（可选直接结局）

## 特别要求
1. 状态继承：必须包含前驱节点的关键状态
2. 冲突升级：比前一层增加紧张度
3. 角色互动：至少2个角色互动
4. 选项设计：大多数节点2个选项，10%以下1个选项

## 输出格式
{{
    &quot;layer&quot;: {},
    &quot;nodes&quot;: [
        {{
            &quot;id&quot;: &quot;字符串数字&quot;,
            &quot;content&quot;: &quot;45-65字第一人称叙述&quot;,
            &quot;characters&quot;: [&quot;角色1&quot;, &quot;角色2&quot;],
            &quot;choices&quot;: [
                {{
                    &quot;text&quot;: &quot;选项文本&quot;,
                    &quot;nextNodeId&quot;: &quot;下一节点ID或结局key&quot;,
                    &quot;affinityEffect&quot;: {{&quot;character&quot;: &quot;角色&quot;, &quot;delta&quot;: 数字}} // 可选
                }}
            ],
            &quot;inherited_states&quot;: [&quot;之前的关键状态&quot;],
            &quot;new_states&quot;: [&quot;本节点新增的状态&quot;]
        }}
    ]
}}
&quot;#,
        layer,
        format_previous_layer(previous_layer),
        format_affinities(story_state),
        format_story_flags(story_state),
        layer,
        calculate_max_nodes(layer),
        calculate_id_range(layer).0,
        calculate_id_range(layer).1,
        get_layer_design_notes(layer),
        layer
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;DeepSeek 方案的缺点&lt;/h4&gt;
&lt;p&gt;此方案实际上与 ChatGPT 基本方案一致, 但实际不如 ChatGPT 方案, 因为&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;剧情大纲完全没有解决节点树结构存在的任何问题&lt;/li&gt;
&lt;li&gt;分层生成实际与每个节点单独生成没有区别, 并没有解决实际问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以是个方案依然是失败的&lt;/p&gt;
&lt;h3&gt;Gemini 有节奏感的剧情树设计&lt;/h3&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://gemini.google.com/share/3359fe7f9880&quot; target=&quot;_blank&quot;&amp;gt;点击这里&amp;lt;/a&amp;gt; 查看完整对话&lt;/p&gt;
&lt;h4&gt;生成纯逻辑的节点树骨架&lt;/h4&gt;
&lt;p&gt;跟 ChatGPT 方案相似, 先生成节点树结构, 每个节点只生成:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nodeId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;level&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;choices&lt;/code&gt; (仅包含 &lt;code&gt;text&lt;/code&gt; 的简写和 &lt;code&gt;nextNodeId&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;剧情摘要 (&lt;code&gt;Summary&lt;/code&gt;): 用一句话描述这个节点发生了什么（例如：“主角潜入实验室被发现”）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里不仅生成了 剧情摘要, 还生成了 每个选项的 内容 / nextNodeId&lt;/p&gt;
&lt;h4&gt;剧本节拍&lt;/h4&gt;
&lt;p&gt;在提示词中加入电影编剧常用的 “三段式结构” 或 “英雄之旅” 约束。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;将 15 个层级划分为：开端 (1-3层)、上升 (4-9层)、高潮 (10-13层)、结局 (14-15层)。每一阶段必须有明确的情感曲线波动。&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体来说, 先对剧情进行拆解, 确定在那一层会出现重要的剧情转折或变化, 再据此进行拆分, 对于一个阶段的节点进行生成&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;有了 ChatGPT / DeepSeek / Gemini 的解决方案, 我们从中提取真正适合的优化方案, 并整理一个合适的优化策略:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前期准备: 毋庸置疑, 我们必须采用 &lt;strong&gt;先规划, 后生成节点&lt;/strong&gt; 的方式, 这也是 LLM 的一致共识, 也就是先生成剧情节点树结构, 再生成节点内容, 这里的难点在于应该生成哪些内容? 首先生成全局的剧本结构, 然后开始和结局节点, 然后生成节点树, 但节点树中的每个节点应该生成摘要吗? 在选项中应该包含状态判断逻辑吗? 然后是剧情状态应该使用什么样的数据结构?&lt;/li&gt;
&lt;li&gt;节点生成: 将与当前节点有关的所有信息作为输入, 输出节点内容, 这里我们需要进一步分析到底应该是 单个节点单独生成, 还是分层生成, 或者分阶段生成&lt;/li&gt;
&lt;li&gt;剧情状态设计: 毫无疑问, 没有剧情状态是不行的, 剧情状态贯穿始终, LLM 没有一致的合适的解决方案, 我们还需要进一步思考剧情状态应该如何设计&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优化策略&lt;/h2&gt;
&lt;h3&gt;剧情状态设计&lt;/h3&gt;
&lt;p&gt;经过深思熟虑, 我放弃了复杂的基于剧情内容的状态设计, 转而使用一种状态:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;s&gt;角色好感度: 每个角色都有一个好感度(&lt;code&gt;0 ~ 100&lt;/code&gt;), 表示角色与玩家的关系, 初始值为 &lt;code&gt;30&lt;/code&gt;, 玩家选择一个选项, 角色的好感度会发生变化, 例如玩家选择喜欢角色, 角色的好感度会增加, 玩家选择讨厌角色, 角色的好感度会减少&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;: 记录玩家已经触发的关键事件(&lt;code&gt;boolean&lt;/code&gt; 值), 也就是基于玩家的选择, 例如玩家选择了某个关键选项, 事件会被记录下来, &lt;strong&gt;后续节点中的选项可以根据事件是否触发来判断指向哪个节点&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
我放弃了角色好感度的设计, 在这里角色好感度其实并不影响剧情, 而是只作为统计值, 其实在真正的电影中, 角色好感度也至关重要, 但是由于它是数值, 在多层级的节点树中的某个中无法根据数值来确切地判断好感度到底是高还是低, 除非把前面所有与此状态相关的节点内容都作为输入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;flags&lt;/h4&gt;
&lt;p&gt;为什么我使用 &lt;code&gt;flags&lt;/code&gt; 来描述关键选择或者关键事件呢? 因为 flags 表示的是状态的转折, 而非关键选择本身, 例如对于一个关键选择: &lt;em&gt;我得到了监狱的钥匙并逃离了监狱&lt;/em&gt;, &lt;code&gt;flags&lt;/code&gt; 指的是 &lt;em&gt;已经越狱&lt;/em&gt; 这个状态, 而不是 &lt;em&gt;我得到了监狱的钥匙&lt;/em&gt; 这个事件, 非常重要的一点是, &lt;code&gt;flags&lt;/code&gt; 只描述状态, 不描述具体内容或者说上下文&lt;/p&gt;
&lt;h4&gt;蝴蝶效应&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-optimize-butterfly.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为 奇异人生1 中的 max, 在游戏中出现的蝴蝶&lt;/p&gt;
&lt;p&gt;在现实生活中, 蝴蝶效应往往不是 &lt;strong&gt;立竿见影&lt;/strong&gt; 的, 而是 &lt;strong&gt;草蛇灰线, 伏脉千里&lt;/strong&gt;. 🦋&lt;/p&gt;
&lt;p&gt;你今天救了一只受伤的流浪猫, 可能在三年后它为你挡下了一次致命的攻击 (有点玄幻了, 但在游戏里很常见).&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;LLM&lt;/code&gt; 生成剧情的上下文中, 实现 &lt;strong&gt;蝴蝶效应&lt;/strong&gt; 的核心在于 &lt;strong&gt;延迟反馈 (Delayed Feedback)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;通常的 &lt;code&gt;LLM&lt;/code&gt; 对话是线性的, 它很难 &quot;记住&quot; 10 轮对话前的一个微小细节. 为了模拟这种效果, 我需要将 &lt;code&gt;flags&lt;/code&gt; 分为两类:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Immediate Flags (即时标记)&lt;/strong&gt;: 影响下一章的剧情走向.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dormant Flags (休眠标记)&lt;/strong&gt;: 像一颗种子埋在 &lt;code&gt;Context&lt;/code&gt; 里, 直到特定的 &lt;code&gt;Level&lt;/code&gt; 或触发条件满足时才 &lt;strong&gt;被唤醒&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;优化节点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;减少节点数量, 增加 level 数量以增加剧情深度&lt;/li&gt;
&lt;li&gt;并发生成节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;增加 acts&lt;/h3&gt;
&lt;p&gt;我们引入 &lt;code&gt;act&lt;/code&gt;(幕/阶段) 的概念, 可以理解为小说的每个大章节, 并且在每个 &lt;code&gt;act&lt;/code&gt; 中的第一层中都只允许存在一个节点, 这也就意味着我们对节点进行了多次强制收束, 并且我们也会在后面生成节点的时候, &lt;strong&gt;并发生成每个幕&lt;/strong&gt;, 这样做看似会导致剧情中的每个章节相互隔离, 但是我们通过通过 先规划剧本 和 引入并规划剧情状态 的方式规避了这个问题&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;h3&gt;1. 生成剧本&lt;/h3&gt;
&lt;p&gt;由于我们生成的剧情节点树非常复杂, 为了提供尽可能确定的信息, 我们需要首先规划全局的剧本结构, 就像我们从北京出发前往纽约, 我们需要指定至少一条路线, 然后再规划好在哪里换乘, 在什么时间休息, 如果遇到可能出现的问题应该如何应对, 这些前期准备是非常重要的&lt;/p&gt;
&lt;h4&gt;Prompts&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 角色定义
你是一位互动电影游戏编剧和总导演, 你擅长创作 引人入胜 / 逻辑严密 / 充满情感冲击力 的多分支剧情

## 主题
{}

## 剧情简稿
{}

## 角色
{}

# 输出
以下是类型定义, 你需要返回的是一个JSON 数据, 具体要求:
- JSON 数据的类型为 `BluePrint`
- **不允许出现任何 `BluePrint` 的类型定义中没有的字段, 绝对不允许出现 nodes**
- **必须认真阅读注释内容, 并严格遵守注释中的要求, 特别是字数或者数量限制**
- 禁止包含 `\n`
- 至少有一个结局节点可以从非最后一个 act 到达
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/** 剧本 */
export interface BluePrint {
  /** 层数, 值为 25-30 */
  levelCount: number
  /** 幕数, 值为 2-3 */
  actCount: number
  /** 起始节点 */
  startNode: StartNode
  /**
   * 关键标记, 数量控制在 1-4 个
   * - key 为标记的名称, 例如 越狱成功, 获得道具等, 必须是确切具体的内容, 不超过 10 个字;
   * - value 为标记的数据
   * @description 这是会影响剧情走向或结局的重要剧情状态, 也是可能造成重要转折, 必须精心设计
   */
  flags: {
    [flagName: string]: {
      /** 标记的详细描述, 必须是确切具体的内容, 不超过 25 字 */
      content: string
      /** 触发/获得 该标记的 level 索引, 值为 2-{@link levelCount}, 必须根据实际剧情({@link acts}) 生成 */
      triggerLevel: number
      /** 此 flag 产生副作用的 level 索引, 值为 {@link triggerLevel}-{@link levelCount}, 必须根据实际剧情({@link acts}) 生成 */
      effectAct: number
    }
  }
  /** 结局节点, 数量控制在 3-5 个, key 为结局的名称, 例如 成功, 失败等; value 为结局信息 */
  endings: {
    [endingName: string]: {
      /** 结局的详细描述, 不超过 35 字 */
      content: string
      /** 触发 该结局的 level 索引, 值为 2-{@link levelCount}, 必须根据实际剧情({@link acts}) 生成 */
      triggerLevel: number
    }
  }
  /**
   * 幕(阶段), 表示相对独立的剧情阶段, 例如 1-5 层为第一阶段, 6-13 层为第二阶段等
   * @description 阶段数量为 {@link actCount} 个
   */
  acts: Array&amp;lt;BluePrintAct&amp;gt;
}

/**
 * 节点 ID, 格式为 `$level-$index`, 例如 `L1N1` 表示第一层中的第一个节点
 * @example L1N1
 */
type NodeId = string

/** 起始节点 */
interface StartNode {
  /** 节点 ID, 值为 L1N1 */
  id: &apos;L1N1&apos;
  /** 节点内容, 必须以第一个主角的第一人称视角编写, 不超过 60 字 */
  content: string
  /** 该节点的角色 name, 数量控制在 1-3 个 */
  characters: Array&amp;lt;string&amp;gt;
  /** 选项列表, 数量为 2 */
  choices: [StartNodeChoice, StartNodeChoice]
}

/** 起始节点的选项 */
interface StartNodeChoice {
  /** 该选项的内容, 必须以第一个主角的第一人称视角编写, 不超过 35 字 */
  content: string
  /** 该选项指向的下一个节点 ID */
  nextNodeId: NodeId
}

/** 剧本中的幕(阶段) */
interface BluePrintAct {
  /**
   * 该阶段的 level 范围(总层数为 {@link levelCount})
   * 例如 [1, 5] 表示此幕为 第 1 到第 5 层, 也就是说包含 1-5 层的所有节点; [6, 12] 表示此幕为 第 6 到第 12 层
   */
  levelRange: [number, number]
  /** 该阶段的名称, 不能超过 10 个字 */
  name: string
  /**
   * 该阶段剧情的完整具体的描述, ⚠️ **必须是确切具体的内容, 禁止任何 可能/模糊/模糊描述/猜测/假设/推测 等内容**, 200-240 字
   * @description 必须 **概括可能出现的所有剧情分支**, 确保不遗漏任何重要信息
   */
  description: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---

根据用户提供的 主题 / 剧情简稿 / 角色, 发挥你的才华 **反复打磨剧情** 并创作一个完整的互动电影剧本, 并根据 [输出](#输出) 中的要求返回 `BluePrint` 类型的 JSON 数据, **禁止在任何内容的首尾出现中文的双引号**
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;剧情信息 Output&lt;/h4&gt;
&lt;p&gt;最终 LLM 输出示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;levelCount&quot;: 28,
  &quot;actCount&quot;: 3,
  &quot;startNode&quot;: {
    &quot;id&quot;: &quot;L1N1&quot;,
    &quot;content&quot;: &quot;后脑勺火辣辣的疼！我茫然抬头，口水浸湿了数学卷子。等等……2014年？黑板上那个数字像一道闪电劈中了我。&quot;,
    &quot;characters&quot;: [&quot;林小北&quot;, &quot;张老师&quot;],
    &quot;choices&quot;: [
      {
        &quot;content&quot;: &quot;震惊地瞪大眼睛，脱口而出：“现在是2014年？”&quot;,
        &quot;nextNodeId&quot;: &quot;L2N1&quot;
      },
      {
        &quot;content&quot;: &quot;强压住翻涌的回忆，低头认错：“对不起张老师，我昨晚复习太晚。”&quot;,
        &quot;nextNodeId&quot;: &quot;L2N2&quot;
      }
    ]
  },
  &quot;flags&quot;: {
    &quot;数学天才觉醒&quot;: {
      &quot;content&quot;: &quot;林小北凭借未来记忆展现超常数学能力&quot;,
      &quot;triggerLevel&quot;: 4,
      &quot;effectAct&quot;: 12
    },
    &quot;与小美约定未来&quot;: {
      &quot;content&quot;: &quot;林小北与苏小美约定考同一所大学&quot;,
      &quot;triggerLevel&quot;: 9,
      &quot;effectAct&quot;: 18
    },
    &quot;拯救大伟危机&quot;: {
      &quot;content&quot;: &quot;林小北提前干预了大伟家庭的财务危机&quot;,
      &quot;triggerLevel&quot;: 16,
      &quot;effectAct&quot;: 23
    },
    &quot;张老师的怀疑&quot;: {
      &quot;content&quot;: &quot;张老师察觉林小北行为异常并展开调查&quot;,
      &quot;triggerLevel&quot;: 20,
      &quot;effectAct&quot;: 26
    }
  },
  &quot;endings&quot;: {
    &quot;重写辉煌&quot;: {
      &quot;content&quot;: &quot;林小北不仅考取顶尖大学，更帮助朋友实现梦想，与苏小美并肩走向未来。&quot;,
      &quot;triggerLevel&quot;: 28
    },
    &quot;遗憾的守护者&quot;: {
      &quot;content&quot;: &quot;林小北过度干涉他人命运导致关系破裂，虽然成绩优异却孤独一人。&quot;,
      &quot;triggerLevel&quot;: 24
    },
    &quot;真相的代价&quot;: {
      &quot;content&quot;: &quot;林小北重生秘密被揭穿，被迫接受心理观察，与理想未来失之交臂。&quot;,
      &quot;triggerLevel&quot;: 22
    },
    &quot;平凡的圆满&quot;: {
      &quot;content&quot;: &quot;林小北放下执念，专注于自身提升，收获了一段充实平和的高三时光。&quot;,
      &quot;triggerLevel&quot;: 26
    }
  },
  &quot;acts&quot;: [
    {
      &quot;levelRange&quot;: [1, 9],
      &quot;name&quot;: &quot;觉醒与试探&quot;,
      &quot;description&quot;: &quot;林小北确认自己重生回到2014年高三。他首先需要适应年轻的身体和校园环境，面对严厉却年轻的班主任张老师。他用成年人的思维应对课堂提问，意外展现出超前的数学知识，这引起了张老师的注意和怀疑。同时，他重新见到了暗恋的苏小美和死党大伟。林小北决定利用未来记忆帮助苏小美提升数学成绩，并试图接近她。他还要应对大伟拉他逃课去网吧的诱惑。在第一次月考中，林小北故意控分，既展示了进步又不过分引人注目。他开始收集信息，确认关键的未来事件节点。本阶段可能出现分支：选择全力学习还是兼顾社交；选择直接追求苏小美还是先提升自己；选择向张老师坦白异常还是隐藏秘密。&quot;
    },
    {
      &quot;levelRange&quot;: [10, 19],
      &quot;name&quot;: &quot;改变与代价&quot;,
      &quot;description&quot;: &quot;林小北开始主动改变命运轨迹。他系统地帮助苏小美复习，两人关系升温，并约定共同的目标大学。同时，他利用未来记忆，通过看似偶然的方式提醒大伟关注其父亲公司的财务风险，试图避免其家庭破产。然而，他的行为开始产生蝴蝶效应：一次提前泄露的考题方向引起了张老师的深度怀疑；他对苏小美过于“了解”的关心让她感到困惑；大伟父亲的公司危机虽然被预警，但处理过程引发了新的问题。林小北必须在学业、友情、萌芽的爱情以及隐藏秘密之间寻找平衡。期中考试成为关键节点，他的成绩将直接影响张老师的下一步行动以及苏小美的态度。本阶段可能出现分支：选择优先处理大伟的危机还是专注自己的学业；选择向苏小美部分坦白还是继续伪装；选择应对张老师的调查时是编造理由还是寻求信任。&quot;
    },
    {
      &quot;levelRange&quot;: [20, 28],
      &quot;name&quot;: &quot;抉择与终局&quot;,
      &quot;description&quot;: &quot;张老师正式与林小北谈话，指出他行为模式与知识储备的异常，逼近真相。大伟家的危机因干预出现新的变数，需要林小北再次介入。苏小美感受到了林小北的压力和秘密，关系出现裂痕或需要更深的信任。高考百日冲刺开始，林小北必须做出最终抉择：是继续利用未来信息获取最大优势，承担秘密暴露的风险；还是回归一个普通高三生的本分，接受命运的不确定性。他的每一个选择都将导向不同结局：可能成功改变所有人的命运，登上巅峰；也可能因过度干预而失去重要的人和事；或者秘密曝光，面临无法预料的后果；亦或在平凡中寻得内心的平静与圆满。最终，在高考考场或毕业时刻，所有的伏笔收束，林小北的重生之旅迎来终局。&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;附 对应的真实输入&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 角色定义
你是一位互动电影游戏编剧和总导演, 你擅长创作 引人入胜 / 逻辑严密 / 充满情感冲击力 的多分支剧情

## 主题
重生之被班主任拍醒

## 剧情简稿
数学课，我趴在课桌上流口水。突然，后脑勺挨了一巴掌：&quot;又睡觉！&quot;猛一抬头，我惊恐地发现——黑板上写着&quot;2014年&quot;，眼前是年轻十岁的班主任老张。我竟然重生了！带着30岁社畜的记忆回到高三，我发誓要改变高考失利的命运，重新追回错过的暗恋，但事情真的会如我所愿吗？

## 角色
\`\`\`json
[
  {
    &quot;name&quot;: &quot;林小北&quot;,
    &quot;description&quot;: &quot;18岁，高三学生（内心30岁）。带着未来的记忆重生，决心改变命运，但成人思维与少年身份格格不入。&quot;,
    &quot;gender&quot;: &quot;男&quot;,
    &quot;isMain&quot;: true
  },
  {
    &quot;name&quot;: &quot;张老师&quot;,
    &quot;description&quot;: &quot;40岁，数学老师兼班主任。严厉古板，口头禅\&quot;你们是我带过最差的一届\&quot;，其实关心学生。&quot;,
    &quot;gender&quot;: &quot;男&quot;,
    &quot;isMain&quot;: false
  },
  {
    &quot;name&quot;: &quot;苏小美&quot;,
    &quot;description&quot;: &quot;18岁，同桌。当年的暗恋对象，活泼可爱，数学很差但很努力，不知道自己已被\&quot;预支\&quot;了未来。&quot;,
    &quot;gender&quot;: &quot;女&quot;,
    &quot;isMain&quot;: false
  },
  {
    &quot;name&quot;: &quot;胖子大伟&quot;,
    &quot;description&quot;: &quot;18岁，死党。讲义气但成绩烂，未来会出国发展，现在是需要帮助的好兄弟。&quot;,
    &quot;gender&quot;: &quot;男&quot;,
    &quot;isMain&quot;: false
  }
]
\`\`\`

# 输出
以下是类型定义, 你需要返回的是一个JSON 数据, 具体要求:
- JSON 数据的类型为 `BluePrint`
- **不允许出现任何 `BluePrint` 的类型定义中没有的字段, 绝对不允许出现 nodes**
- **必须认真阅读注释内容, 并严格遵守注释中的要求, 特别是字数或者数量限制**
- 禁止包含 `\n`
- 至少有一个结局节点可以从非最后一个 act 到达

\`\`\`typescript
/** 剧本 */
export interface BluePrint {
  /** 层数, 值为 25-30 */
  levelCount: number
  /** 幕数, 值为 2-3 */
  actCount: number
  /** 起始节点 */
  startNode: StartNode
  /**
   * 关键标记, 数量控制在 1-4 个
   * - key 为标记的名称, 例如 越狱成功, 获得道具等, 必须是确切具体的内容, 不超过 10 个字;
   * - value 为标记的数据
   * @description 这是会影响剧情走向或结局的重要剧情状态, 也是可能造成重要转折, 必须精心设计
   */
  flags: {
    [flagName: string]: {
      /** 标记的详细描述, 必须是确切具体的内容, 不超过 25 字 */
      content: string
      /** 触发/获得 该标记的 level 索引, 值为 2-{@link levelCount}, 必须根据实际剧情({@link acts}) 生成 */
      triggerLevel: number
      /** 此 flag 产生副作用的 level 索引, 值为 {@link triggerLevel}-{@link levelCount}, 必须根据实际剧情({@link acts}) 生成 */
      effectAct: number
    }
  }
  /** 结局节点, 数量控制在 3-5 个, key 为结局的名称, 例如 成功, 失败等; value 为结局信息 */
  endings: {
    [endingName: string]: {
      /** 结局的详细描述, 不超过 35 字 */
      content: string
      /** 触发 该结局的 level 索引, 值为 2-{@link levelCount}, 必须根据实际剧情({@link acts}) 生成 */
      triggerLevel: number
    }
  }
  /**
   * 幕(阶段), 表示相对独立的剧情阶段, 例如 1-5 层为第一阶段, 6-13 层为第二阶段等
   * @description 阶段数量为 {@link actCount} 个
   */
  acts: Array&amp;lt;BluePrintAct&amp;gt;
}

/**
 * 节点 ID, 格式为 `$level-$index`, 例如 `L1N1` 表示第一层中的第一个节点
 * @example L1N1
 */
type NodeId = string

/** 起始节点 */
interface StartNode {
  /** 节点 ID, 值为 L1N1 */
  id: &apos;L1N1&apos;
  /** 节点内容, 必须以第一个主角的第一人称视角编写, 不超过 60 字 */
  content: string
  /** 该节点的角色 name, 数量控制在 1-3 个 */
  characters: Array&amp;lt;string&amp;gt;
  /** 选项列表, 数量为 2 */
  choices: [StartNodeChoice, StartNodeChoice]
}

/** 起始节点的选项 */
interface StartNodeChoice {
  /** 该选项的内容, 必须以第一个主角的第一人称视角编写, 不超过 35 字 */
  content: string
  /** 该选项指向的下一个节点 ID */
  nextNodeId: NodeId
}

/** 剧本中的幕(阶段) */
interface BluePrintAct {
  /**
   * 该阶段的 level 范围(总层数为 {@link levelCount})
   * 例如 [1, 5] 表示此幕为 第 1 到第 5 层, 也就是说包含 1-5 层的所有节点; [6, 12] 表示此幕为 第 6 到第 12 层
   */
  levelRange: [number, number]
  /** 该阶段的名称, 不能超过 10 个字 */
  name: string
  /**
   * 该阶段剧情的完整具体的描述, ⚠️ **必须是确切具体的内容, 禁止任何 可能/模糊/模糊描述/猜测/假设/推测 等内容**, 200-240 字
   * @description 必须 **概括可能出现的所有剧情分支**, 确保不遗漏任何重要信息
   */
  description: string
}
\`\`\`
---

根据用户提供的 主题 / 剧情简稿 / 角色, 发挥你的才华 **反复打磨剧情** 并创作一个完整的互动电影剧本, 并根据 [输出](#输出) 中的要求返回 `BluePrint` 类型的 JSON 数据, **禁止在任何内容的首尾出现中文的双引号**
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 生成逻辑拓扑&lt;/h3&gt;
&lt;p&gt;接下来我们要构建具体的 剧情树结构, 在 &amp;lt;a href=&quot;/movie-games-record&quot; target=&quot;_blank&quot;&amp;gt;上篇文章&amp;lt;/a&amp;gt; 中我们介绍了剧情树接口的要求, 以及因 &lt;code&gt;LLM&lt;/code&gt; 输出的不稳定性可能导致的诸多问题, 因此我们尝试单独生成剧情树, 也就是逻辑拓扑结构, 我们有两个选择:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;让 LLM 使用只专注于生成剧情树结构, 尽可能少的关注剧情内容&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;优势:
&lt;ul&gt;
&lt;li&gt;可以根据实际的剧情信息生成更符合剧情的逻辑拓扑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;劣势:
&lt;ul&gt;
&lt;li&gt;输出不稳定, 可能会生成不符合要求的逻辑拓扑&lt;/li&gt;
&lt;li&gt;更长的耗时&lt;/li&gt;
&lt;li&gt;消耗更多的 token&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;直接通过程序生成剧情树结构&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;优势:
&lt;ul&gt;
&lt;li&gt;可以生成绝对稳定的逻辑拓扑&lt;/li&gt;
&lt;li&gt;生成速度非常快&lt;/li&gt;
&lt;li&gt;不消耗 token&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;劣势:
&lt;ul&gt;
&lt;li&gt;不能根据实际的剧情信息生成更符合剧情的逻辑拓扑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.1 LLM 生成逻辑拓扑&lt;/h4&gt;
&lt;p&gt;在这个阶段, 只生成剧情树的 &lt;strong&gt;骨架 (Skeleton)&lt;/strong&gt;, 也就是 &lt;code&gt;Topology&lt;/code&gt; (拓扑结构). 它不需要包含具体的对话内容, 只需要包含:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: 节点 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;summary&lt;/code&gt;: 一句话剧情梗概 (One-sentence Summary)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;choices&lt;/code&gt;: 选项结构 (仅包含跳转逻辑 &lt;code&gt;nextNodeId&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;required_flags&lt;/code&gt;: 进入该节点需要的条件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 剧情树骨架节点定义
interface StoryNodeSkeleton {
  id: string;
  beatType: &apos;Setup&apos; | &apos;Conflict&apos; | &apos;Climax&apos; | &apos;Resolution&apos;;
  summary: string; // 例如: &quot;主角在废弃仓库发现了一张旧照片&quot;
  choices: {
    label: string; // 简短的选项标签, 如 &quot;捡起来&quot;
    nextNodeId: string;
  }[];
  // ... 其他元数据
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;strong&gt;为什么要这么做?&lt;/strong&gt;
因为 &lt;code&gt;LLM&lt;/code&gt; 的注意力是有限的. 如果让它同时思考 &quot;这句话该怎么写才优美&quot; 和 &quot;这个选项该跳到第几层才不会死循环&quot;, 它往往两头都顾不好.
通过剥离 &lt;strong&gt;文学创作&lt;/strong&gt; 任务, 让它专注于 &lt;strong&gt;逻辑构建&lt;/strong&gt;, 我们可以得到一个结构非常严谨、甚至带有精妙 &lt;strong&gt;收束点 (Convergence Point)&lt;/strong&gt; 的剧情树. 🌳
:::&lt;/p&gt;
&lt;p&gt;在这个阶段, 我会强制要求 &lt;code&gt;LLM&lt;/code&gt; 检查每一层的 &lt;strong&gt;节点数量&lt;/strong&gt; 和 &lt;strong&gt;收束率&lt;/strong&gt;. 如果发现某一层节点爆炸 (Node Explosion), 或者某条线断了 (Dead End), 直接在代码层面这就给它 &lt;code&gt;Reject&lt;/code&gt; 掉, 根本不需要等到生成正文.&lt;/p&gt;
&lt;h5&gt;确定结构要求&lt;/h5&gt;
&lt;p&gt;首先我们需要明确节点树的具体要求, 这样才能让 LLM 发挥作用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你是一位严谨的游戏逻辑架构师, 你的任务是根据提供的[剧情介绍](#剧情介绍), 为互动游戏生成纯逻辑的拓扑结构（DAG）

**你需要认真阅读 [介绍](#介绍) 和 [具体要求](#具体要求) 中的内容, 并根据内容生成符合要求的剧情树 JSON**
---
## 主题
{}

## 剧情介绍
{}

## 基本概念
### level
节点的层级, 层级表示节点在剧情树中的深度, 例如 `level` 为 1 表示第一层, 第一层只有一个开始节点, 开始节点中的选项(`choices`)指向下一个层级的节点(或者结局节点)

### node
节点, 分为剧情节点和结局节点

### act
幕(阶段), 表示相对独立的剧情阶段, 例如 `1-5` 层为第一阶段, `6-13` 层为第二阶段等; 用于控制节点的数量和收束率

### flag
关键标记, 意为选择某个选项之后触发的 **影响当前或后续剧情走向** 的事件, 数量在 `1-4` 个

#### 剧情节点
剧情节点, 表示剧情的发展过程, 类型定义如下:

\`\`\`typescript
type StoryNodeList = [StoryNode]
interface StoryNode {
  /** 节点 ID, 从第二层(L2)开始 */
  id: NodeId;
  /** 节点的剧情梗概, 不超过 20 字 */
  summary: string
  /** 选项列表, 数量 */
  choices: [Choice, Choice]
}
/**
 * 节点 ID, 格式为 `$level-$index`, 例如 `L1N1` 表示第一层中的第一个节点
 * @example L1N1
 */
type NodeId = string
/**
 * 结局节点名称, 例如 &apos;完美人生&apos; 对应 `endings` 中的 &apos;完美人生&apos; 结局
 */
type EndNodeId = string
interface Choice {
  /** 该选项的内容, 先不生成内容 */
  content: &apos;&apos;
  /** 该选项指向的下一个节点 ID(或结局节点 ID) */
  nextNodeId: NodeId | EndNodeId
  /**
   * 当获得指定的(一个或两个) flag 时, 需要跳转的下一个节点 ID(或结局节点 ID)
   * @description `key` 为下一个节点 ID(或结局节点 ID), `value` 为触发跳转需要的全部 flag; **`key` 不能与 `nextNodeId` 相同**
   * @example `{ &quot;L10N2&quot;: [&quot;学业觉醒&quot;] }` - 当获得 `学业觉醒` 这个 flag 时, 跳转至 `L10N2` 节点
   * @example `{ &quot;L10N3&quot;: [&quot;青春悸动&quot;] }` - 当获得 `青春悸动` 这个 flag 时, 跳转至 `L10N3` 节点
   * @example `{ &quot;不幸遇难&quot;: [&quot;命悬一线&quot;, &quot;迷失方向&quot;] }` - 当获得 `命悬一线` 和 `迷失方向` 这个 flag 时, 跳转至 `不幸遇难` 结局节点
   */
  plotBranching?: {
    [nextNodeId: NodeId | EndNodeId]: [string] | [string, string]
  }
  /** 选择当前节点之后获取到的 flag */
  flag?: string
}
\`\`\`

#### 结局节点
现有的结局节点为:
{}

## 具体要求
- 每层节点数量控制在 `1-3` 个, 至少 `70%` 的 level 层节点数量为 `2` 个
- 必须是 DAG（有向无环图）
- 所有选项都必须指向下一个层级的节点(或者结局节点)
- 所有的 `flag` 必须有触发节点和应用节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;最终输出结果&lt;/h5&gt;
&lt;p&gt;我测试了一下 &amp;lt;a href=&quot;https://chat.deepseek.com/share/nbodpwrki54a5coulb&quot; target=&quot;_blank&quot;&amp;gt;deepseek 的输出&amp;lt;/a&amp;gt;, 发现 LLM 根本无法生成结构严谨的逻辑拓扑, 出现了 &amp;lt;a href=&quot;/movie-games-record&quot; target=&quot;_blank&quot;&amp;gt;上篇文章&amp;lt;/a&amp;gt; 一样的问题&lt;/p&gt;
&lt;h4&gt;2.2 通过程序生成逻辑拓扑&lt;/h4&gt;
&lt;p&gt;下面我们来通过程序实现一个简单的逻辑拓扑生成方法, 对应的输入就是 &lt;a href=&quot;#%E5%89%A7%E6%83%85%E4%BF%A1%E6%81%AF-output&quot;&gt;剧情信息 Output&lt;/a&gt;, 输出就是一个不包含实际内容的逻辑拓扑 JSON 字符串&lt;/p&gt;
&lt;h5&gt;逻辑拓扑数据类型&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/** 至少包含一个元素的数组（非空数组） */
type NonEmptyArray&amp;lt;T&amp;gt; = readonly [T, ...T[]]

/**
 * 第一行只能有一个元素，后续每一行至少一个元素的二维数组
 */
type RestrictedTwoDimensionalArray&amp;lt;T&amp;gt; = readonly [
  readonly [T],
  ...NonEmptyArray&amp;lt;T&amp;gt;[]
]

/**
 * 包含全部剧情节点的 分层有向无环图 (`Layered Directed Acyclic Graph`)
 * @description 具有严格详细数学约束的 **分层有向无环图** 剧情节点图
 */
export type LDAGActs = NonEmptyArray&amp;lt;LDAGNodes&amp;gt;

/**
 * 每一幕的剧情节点图
 */
export type LDAGNodes = RestrictedTwoDimensionalArray&amp;lt;LDAGNode&amp;gt;

/**
 * 结局节点集合
 * @description endingName 为结局节点名称, EndingNode 为结局节点信息
 */
export type EndingNodes = {
  [endingName: `ENDING_${string}`]: EndingNode
}

/** 结局节点 */
interface EndingNode {
  /** 结局的详细描述, 不超过 35 字 */
  content: string
  /** 触发 该结局的 level 索引 */
  triggerLevel: number
}

/**
 * 节点 ID, 格式为 `$level-$index`, 例如 `L1N1` 表示第一层中的第一个节点
 * @description level 表示第几层, index 表示该层中的第几个节点(从 1 开始索引)
 * @example L1N1
 */
type NodeId = `L${number}N${number}`

/** 结局节点 ID */
type EndingNodeId = `ENDING_${string}`

/**
 * LDAG 分层有向无环图节点
 */
interface LDAGNode {
  /**
   * 节点 ID, 格式为 `$level-$index`, 例如 `L1N1` 表示第一层中的第一个节点
   * @example L1N1 起始节点
   * @example L2N2 位于第二层的第二个节点(索引为 2 的节点)
   */
  id: NodeId
  /** 节点内容, 不超过 50 字 */
  content: string
  /** 该节点包含或关联的角色 name, 数量控制在 1-3 个 */
  characters: Array&amp;lt;string&amp;gt;
  /**
   * 选项列表, 数量为 1-3
   * ## 允许节点收束
   * 允许选项指向相同的节点
   */
  choices: [LDAGNodeChoice] | [LDAGNodeChoice, LDAGNodeChoice] | [LDAGNodeChoice, LDAGNodeChoice, LDAGNodeChoice]
}

/** 节点的选项 */
interface LDAGNodeChoice {
  /** 该选项的内容, 不超过 25 字 */
  content: string
  /** 
   * 触发(设置)的 flag 名称 
   * @description 对应 BluePrint 中的 flags[flagName].triggerLevel
   */
  triggerFlag?: string
  /** 
   * 该选项指向的下一个节点 ID 
   * @description 如果是 string, 表示无条件跳转; 如果是对象, 表示根据 checkFlag 的值跳转(对应 BluePrint 中的 flags[flagName].effectLevel)
   */
  nextNodeId: NodeId | EndingNodeId | ConditionalNextNodeId
}

/** 条件跳转节点 ID */
interface ConditionalNextNodeId {
  /** 需要检查的 flag 名称 */
  checkFlag: string
  /** flag 为 true 时的下一个节点 ID */
  trueId: NodeId | EndingNodeId
  /** flag 为 false 时的下一个节点 ID */
  falseId: NodeId | EndingNodeId
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;完整的数学约束&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;# 剧情节点图的数据结构

以下是该分层有向无环图（Layered Directed Acyclic Graph, **LDAG**）剧情结构的严谨数学约束定义：

### 剧情系统 LDAG 数学约束模型

设剧情图为 $G = (V_P, V_E, E)$，其中 $V_P$ 为剧情节点集，$V_E$ 为结局节点集，$E$ 为有向边集。

#### 1. 集合定义与层级分区

* **幕与层级分区**：剧情图 $G$ 由 $m$ 个有序的幕（Act）组成：$G = \{A_1, A_2, \dots, A_m\}$，其中 $3 \le m \le 4$。
* **节点分区**：每一幕 $A_i$ 的节点集 $V_{P_i}$ 被划分为 $k_i$ 个不相交的子集（层）：$V_{P_i} = L_{i,1} \cup L_{i,2} \cup \dots \cup L_{i,k_i}$，其中 $8 \le k_i \le 15$。总层数 $k = \sum k_i$ 满足 $35 \le k \le 45$。
* **幕起始节点**：每一幕的第一层 $L_{i,1}$ 必须且只能包含一个节点 $\{v_{start_i}\}$。即 $|L_{i,1}| = 1$。
* **结局隔离**：结局节点集 $V_E$ 与剧情节点集 $V_P$ 互斥，$V_P \cap V_E = \emptyset$。且结局节点入度 $d^-(v) &amp;gt; 0$，出度 $d^+(v) = 0$（仅作为叶子节点）。

#### 2. 拓扑分布约束 (Density Control)

* **层宽约束**：对于任意层 $L_i \in V_P$，其基数满足 $1 \le |L_i| \le 3$。
* **比例分布**：满足条件 $|L_i| = 2$ 的层数占比 $P(|L_i|=2) \ge 70\%$。

#### 3. 边的单调性与跨度约束 (Edge Monotonicity)

对于任意有向边 $e = (u, v) \in E$：

* **严格递增**：若 $u \in L_i$ 且 $v \in L_j$（或 $v \in V_E$），则必须满足 $j &amp;gt; i$。
* **禁止环路**：由于 $j &amp;gt; i$，图 $G$ 物理上不存在环路（Acyclic）。
* **邻层倾向**：设 $E_{adj}$ 为满足 $j = i+1$ 的边集，则要求其在剧情节点间的边占比满足 $\frac{|E_{adj} \cap (V_P \times V_P)|}{|E \cap (V_P \times V_P)|} \ge 90\%$。

#### 4. 强连通性约束 (Connectivity)

* **全可达性**：对于任意节点 $v \in V_P \cup V_E$，必存在至少一条路径 $Path(v_{start} \to v)$。即 $G$ 中不存在孤立点或入度为 0 的非起始节点。
* **结局可达性**：对于任意结局 $v_e \in V_E$，必存在路径 $Path(u \to v_e)$，其中 $u \in V_P$。
* **禁止初级终结**：不存在边 $(v_{start}, v_e)$，其中 $v_{start} \in L_1, v_e \in V_E$。

#### 5. 状态机闭环约束 (Flag Logic)

设全局布尔标记集为 $F = \{f_1, f_2, \dots, f_m\}$，其中 $3 \le m \le 6$。
对于任意 $f \in F$：

* **定义域闭环**：标记 $f$ 必须在边集 $E$ 的属性中至少被执行一次 $Set(f)$ 操作和一次 $Check(f)$ 操作。
* **因果序约束**：设 $SetNodes(f) = \{u \in V_P \mid \exists (u, v) \text{ s.t. } Set(f)\}$，设 $CheckNodes(f) = \{u \in V_P \mid \exists (u, v) \text{ s.t. } Check(f)\}$。则必须满足：$$\min_{u \in SetNodes(f)} (Level(u)) &amp;lt; \min_{w \in CheckNodes(f)} (Level(w))$$


* **判定完备性**：所有 $Check(f)$ 操作必须提供完备的二元输出路径 $\{if\_true, if\_false\}$，且两个出口指向的节点 $v$ 必须满足层级约束。
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;输出&lt;/h5&gt;
&lt;p&gt;最终的输出不包含剧情节点的内容, 仅包含节点 ID, 角色, 选项, 跳转目标等信息&lt;/p&gt;
&lt;h3&gt;3 生成节点内容&lt;/h3&gt;
&lt;p&gt;有了前面生成的 剧本 和 逻辑拓扑, 我们就可以填充每个节点的剧情了, 这里我们选择每个 &lt;code&gt;act&lt;/code&gt;(幕) 单独生成, 也就是所有的 &lt;code&gt;acts&lt;/code&gt; 并发生成, 提升速度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 角色定义
你是一位互动电影游戏编剧和总导演, 你擅长创作 引人入胜 / 逻辑严密 / 充满情感冲击力 的多分支剧情

## 主题
__TITLE__

## 剧情简稿
__SUMMARY__

## 角色
__CHARACTERS__

## 当前幕剧情信息
__ACT_INFO__

## 任务
你需要根据给定的 **剧情节点图结构** 和 **当前幕剧情信息**, 为每个节点填充具体的 **剧情内容** 和 **选项内容**。

## 剧情节点图结构
__LDAG_NODES__

## 输出要求
请输出符合以下 TypeScript 类型定义的 JSON 数据, 也就是一个 `LDAGNodes` 类型的 JSON 数据:

\`\`\`typescript
__LDAG_TYPES__
\`\`\`

## 注意事项
1. 保持剧情的连贯性和逻辑性, 必须符合 **当前幕剧情信息** 的描述。
2. 节点的 ID 和连接关系 **必须** 与输入的 **剧情节点图结构** 完全一致，不能增加、删除或修改节点和连线，只能填充内容。
3. `content` 字段为剧情文本。
4. `choices` 字段中的 `content` 为选项文本。
5. 严格遵守 JSON 格式输出。

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4 返回完整的剧情信息&lt;/h3&gt;
&lt;p&gt;最后, 我们定义一下接口的最终返回值类型:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { EndingNodes, LDAGActs } from &quot;./LayeredDirectedAcyclicGraph&quot;;
import { type BluePrint } from &apos;./BluePrint&apos;

/** 完整的生成完毕后的剧情信息 */
export interface Story extends BluePrint {
  /** 剧情节点图 JSON */
  actList: LDAGActs
  /** 结局节点集合 */
  endings: EndingNodes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;完整计划稿&lt;/h3&gt;
&lt;p&gt;可以在我的仓库中看到完整的 &amp;lt;a href=&quot;https://github.com/SublimeCT/movie-games/blob/feature/main-generating/.claude/plan/generating/generating.md&quot; target=&quot;_blank&quot;&amp;gt;plan 文件&amp;lt;/a&amp;gt;(最终是由 &lt;code&gt;trae&lt;/code&gt; 国际版完成的任务)&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-optimize-summary.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过这一系列的重构，我终于从 &quot;把所有东西一股脑扔给 LLM&quot; 的暴力美学中醒悟过来。我们总是期待 LLM 能成为那颗 &lt;strong&gt;银弹&lt;/strong&gt;，能够一次性解决所有问题，但现实往往是它给了你一坨不可控的 &lt;code&gt;JSON&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;构建工作流 (&lt;code&gt;Workflow&lt;/code&gt;) 本质上是在给 LLM &lt;strong&gt;降噪&lt;/strong&gt; 和 &lt;strong&gt;祛魅&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;祛魅&lt;/strong&gt;: 承认 LLM 的局限性，它不是全能的上帝，它记不住 80 个节点前的伏笔，也算不清复杂的概率收束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降噪&lt;/strong&gt;: 通过拆解任务，让 LLM 在每一个 &lt;code&gt;Stage&lt;/code&gt; 只专注于一件事——要么是天马行空的剧本创作，要么是严谨的逻辑填空。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在的流程虽然看起来比最初的 &quot;一键生成&quot; 复杂了数倍:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先写剧本 (&lt;code&gt;BluePrint&lt;/code&gt;) 📝&lt;/li&gt;
&lt;li&gt;程序生成骨架 (&lt;code&gt;Topology&lt;/code&gt;) 🦴&lt;/li&gt;
&lt;li&gt;并发填充血肉 (&lt;code&gt;Content&lt;/code&gt;) 🥩&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但这种 &lt;strong&gt;&quot;程序逻辑约束(Math) + AI 创意填充(Magic)&quot;&lt;/strong&gt; 的模式，才是目前构建复杂 AI 应用的正确打开方式。程序保证了下限（至少能跑通，逻辑闭环），而 LLM 决定了上限（剧情是否精彩）。&lt;/p&gt;
&lt;p&gt;如果说上一篇文章是理想主义的碰壁，那这一篇就是工程主义的胜利。当然，这套方案也不是终点，生成的剧情依然可能存在逻辑漏洞，但至少，我们现在有了一个可以稳定迭代的 &lt;strong&gt;基座&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;接下来的挑战，就是如何让生成的游戏更好玩，以及如何把这个生成器真正做成一个产品。🚀&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;/movie-games-record&quot; target=&quot;_blank&quot;&amp;gt;上一篇文章&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;./assets/images/movie-games-optimize-mow.excalidraw.svg&quot; target=&quot;_blank&quot;&amp;gt;现有的剧情树设计&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;../movie-games-record/#剧情状态丢失&quot; target=&quot;_blank&quot;&amp;gt;剧情状态丢失&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chatgpt.com/share/6964a414-ade4-800b-b839-5b53fd8a1ce4&quot; target=&quot;_blank&quot;&amp;gt;ChatGPT&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://gemini.google.com/share/3359fe7f9880&quot; target=&quot;_blank&quot;&amp;gt;Gemini&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chat.deepseek.com/share/pysplefy24u9rk71gc&quot; target=&quot;_blank&quot;&amp;gt;DeepSeek&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chatgpt.com/share/6964a414-ade4-800b-b839-5b53fd8a1ce4&quot; target=&quot;_blank&quot;&amp;gt;点击这里&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chat.deepseek.com/share/pysplefy24u9rk71gc&quot; target=&quot;_blank&quot;&amp;gt;点击这里&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://gemini.google.com/share/3359fe7f9880&quot; target=&quot;_blank&quot;&amp;gt;点击这里&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;/movie-games-record&quot; target=&quot;_blank&quot;&amp;gt;上篇文章&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://chat.deepseek.com/share/nbodpwrki54a5coulb&quot; target=&quot;_blank&quot;&amp;gt;deepseek 的输出&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/SublimeCT/movie-games/blob/feature/main-generating/.claude/plan/generating/generating.md&quot; target=&quot;_blank&quot;&amp;gt;plan 文件&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>AI 互动剧情类游戏生成工具开发记录1 - 剧情设计</title><link>http://blog.xiaban.run/posts/2026/movie-games-record/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2026/movie-games-record/</guid><description>前段时间 vibe coding 了一个生成互动剧情类游戏的生成工具, 在开发过程中遇到了一系列问题, 本文将对具体的问题进行介绍</description><pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前段时间 &lt;code&gt;vibe coding&lt;/code&gt; 了一个&amp;lt;a href=&quot;http://movie-games.xiaban.run/&quot; target=&quot;_blank&quot;&amp;gt;🔗 互动剧情类游戏的生成工具&amp;lt;/a&amp;gt;, &amp;lt;a ref=&quot;https://movie-games.xiaban.run/play/2b22e895-ce32-488e-a9fe-a015355eb2ae&quot;&amp;gt;点此游玩&amp;lt;/a&amp;gt;, 在开发过程中遇到了一系列问题, 本文将对具体的问题进行介绍&lt;/p&gt;
&lt;h2&gt;互动剧情类游戏&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%BA%92%E5%8A%A8%E7%94%B5%E5%BD%B1%E6%B8%B8%E6%88%8F&quot; target=&quot;_blank&quot;&amp;gt;互动电影游戏&amp;lt;/a&amp;gt; 是一个游戏类型, 与此相同的称呼还有: &lt;em&gt;剧情互动类游戏&lt;/em&gt; / &lt;em&gt;互动电影游戏&lt;/em&gt; / &lt;em&gt;交互式小说&lt;/em&gt; / &lt;em&gt;互动小说&lt;/em&gt; 等等&lt;/p&gt;
&lt;p&gt;其实我真正想做的是有视频的互动剧情类游戏, 这才是这类游戏的完整形态, 但受限于视频和图片生成的成本, 我最终选择了只生成剧情和角色头像的版本, 那么 &lt;strong&gt;什么是互动剧情类游戏呢?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-life-strange.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们不讲晦涩难懂的专业名词, 而是列举几个同类型的游戏:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/curator/36149206&quot;&amp;gt;奇异人生 系列&amp;lt;/a&amp;gt;: 主角拥有超能力, 剧情非常沉浸式, 你可以控制主角的超能力, 逐渐了解游戏的世界观, &lt;strong&gt;每个人物都有精心的塑造, 每个选择都可能引发蝴蝶效应&lt;/strong&gt;, 游戏工作室将大多数资金用于剧情创作和聘请配音员&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/1222140/_/&quot; target=&quot;_blank&quot;&amp;gt;底特律：化身为人&amp;lt;/a&amp;gt;: 游戏背景是人类与机器人的共处的未来, 你将扮演三个人形仿生机器人与人类相处, 站在机器人的角度思考和处理与人类的关系, &lt;strong&gt;这个游戏让我感觉人类是自私和愚蠢的, 机器人也许会替代人类完成人类的使命&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/939400/LoveChoice/?l=schinese&quot; target=&quot;_blank&quot;&amp;gt;拣爱&amp;lt;/a&amp;gt;: &lt;em&gt;这其实不算电影类游戏&lt;/em&gt;, 拣爱是一款深受日式AVG游戏启发的剧情导向游戏, &lt;strong&gt;它强调玩家的每一个选择，每个场景都充满了深意和目的性&lt;/strong&gt;。这款游戏以探讨爱为主题，旨在通过其丰富的故事和情感体验，引导玩家理解爱的真谛，并学习如何在人际关系中表达和接受爱。通过《拣爱》，玩家将踏上一段关于情感、选择与爱的旅程。&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/2322560/_/&quot; target=&quot;_blank&quot;&amp;gt;完蛋! 我被美女包围了!&amp;lt;/a&amp;gt;: 无需多言, 它完全没有故事情节, 但它有足够漂亮主动的女生, 和爽文男主般的初始设定, &lt;strong&gt;顶级的男主, 一流的女演员, 蹩脚的台词, 和敷衍的剧情&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/1341820/As_Dusk_Falls/&quot; target=&quot;_blank&quot;&amp;gt;日落黄昏时(As Dusk Falls)&amp;lt;/a&amp;gt;: 这是我玩过的第一个互动电影类游戏, 我至今无法忘记那个一口气通关的下午, 它与其他同类型游戏的不同之处在于, 它的沉浸感非常强, &lt;strong&gt;它让你感觉到就像一个专业的电影团队做起了游戏, 它有着成熟的剧本, 成熟的配音, 成熟的画面, 一切都是专业级的&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/1577120/_/&quot; target=&quot;_blank&quot;&amp;gt;采石场惊魂&amp;lt;/a&amp;gt;: 这是我玩过的唯一一个恐怖类型的互动电影类游戏, &lt;strong&gt;它对于恐怖元素的尺度把握的刚刚好&lt;/strong&gt;, 喜欢恐怖游戏的朋友会感觉很过瘾&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结一下这类游戏的特点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;沉浸式&lt;/strong&gt; 的电影级体验&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多故事线/多结局&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互动性强&lt;/strong&gt;, 玩家可以根据自己的选择影响故事走向&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI 驱动的互动剧情类游戏&lt;/h2&gt;
&lt;p&gt;随着生成式人工智能的发展, 互动剧情类游戏会出现哪些变化呢? 我们把游戏拆分为三部分:&lt;/p&gt;
&lt;h3&gt;📚 剧本&lt;/h3&gt;
&lt;p&gt;一个成熟的工业级的电影剧本必然会 &lt;strong&gt;经历多轮修改&lt;/strong&gt;, 剧本的修改或打磨往往跟 导演 / 编剧 密不可分, 我认为现阶段最大的问题在于:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;剧本内容过长, 现阶段 LLM 还无法直接处理拥有巨量文字的剧本, 换句话说 &lt;strong&gt;巨大的上下文会导致 AI 忘记某些事 或者 出现幻觉&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 生成的 剧情 / 角色 缺乏张力&lt;/strong&gt;, 剧情过于普通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 对潜台词的理解是 形式化 的&lt;/strong&gt;, 电影中经常出现的潜台词需要结合 情绪 / 目的 / 角色冲突 等一系列因素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 AI 难以直接生成替代人类创作和打磨的剧本&lt;/p&gt;
&lt;h3&gt;🎬 电影&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;现阶段各大厂都推出了自己的视频模型, 例如 &amp;lt;a href=&quot;https://gemini.google/tw/overview/video-generation/?hl=zh-TW&quot; target=&quot;_blank&quot;&amp;gt;video-generation&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://www.adobe.com/tw/products/firefly/features/ai-video-generator.html&quot; target=&quot;_blank&quot;&amp;gt;Adobe Firefly&amp;lt;/a&amp;gt;, 而且不约而同的标榜自己可以进行电影级别的视频生成, 但只要你仔细观察生成的视频, 就会发现一些违背现实世界物理规律的地方, 但这不妨碍用于为视频增加特效等应用场景, 我认为一定会有部分特效非常适合 AI 生成&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-site.png&quot; alt=&quot;&quot; /&gt;
如何制作一部工业级的电影?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开发期: 把一个 &lt;strong&gt;想法&lt;/strong&gt; 变成可拍摄、可融资、可评估风险的项目&lt;/li&gt;
&lt;li&gt;前期制作: 在不开机的情况下，把所有问题提前解决&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;导演确定视觉风格（Reference / Lookbook）&lt;/li&gt;
&lt;li&gt;分镜（Storyboard / Previs）&lt;/li&gt;
&lt;li&gt;演员选角（Casting）&lt;/li&gt;
&lt;li&gt;勘景（Location Scouting）&lt;/li&gt;
&lt;li&gt;摄影、美术、服装、化妆方案&lt;/li&gt;
&lt;li&gt;拍摄计划（Shooting Schedule）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;拍摄期: 在时间 / 预算 / 人员约束下完成素材采集&lt;/li&gt;
&lt;li&gt;后期制作: 把素材变成叙事流畅、情绪准确、节奏稳定的成片&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;电影实际上不是拍出来的, 而是剪出来的, 这几乎完全依赖制作人员的经验和审美&lt;/strong&gt;, 电影风格或者说审美是难以完全交给 AI&lt;/p&gt;
&lt;h3&gt;🎮 游戏&lt;/h3&gt;
&lt;p&gt;在剧本已经确定, 视频已经制作完成之后, 游戏软件(GUI) 本身的制作就回归到软件开发领域了, 在代码层面就非常适合 AI 来接管&lt;/p&gt;
&lt;h2&gt;软件设计&lt;/h2&gt;
&lt;p&gt;我要做的是一个 AI 驱动的互动剧情类游戏 &lt;strong&gt;生成器&lt;/strong&gt;, 为什么我没有说 互动电影游戏 呢? 因为图片和视频的生成成本过高耗时太长, 我想做的是一个 &lt;strong&gt;快速可玩的生成工具&lt;/strong&gt;, 剧本生成也存在难以解决的问题, 所以我真正要做的是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只有 背景图 / 角色头像&lt;/li&gt;
&lt;li&gt;多故事线的剧情树&lt;/li&gt;
&lt;li&gt;可以设计剧情信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;页面&lt;/h3&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;点击查看全部页面&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;主页
&lt;img src=&quot;./assets/images/movie-games-record-home-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成页
&lt;img src=&quot;./assets/images/movie-games-record-generating-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;游戏页
&lt;img src=&quot;./assets/images/movie-games-record-game-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结局页
&lt;img src=&quot;./assets/images/movie-games-record-ending-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设计页
&lt;img src=&quot;./assets/images/movie-games-record-design-page.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/movie-games-record-design2-page.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/movie-games-record-edit-node-page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;数据流&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-dataflow.excalidraw.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;图片生成&lt;/h3&gt;
&lt;p&gt;图片生成使用 GLM 的免费(限制并发)文生图模型 &amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/guide/models/free/cogview-3-flash&quot; target=&quot;_blank&quot;&amp;gt;cogview-3-flash&amp;lt;/a&amp;gt;, 虽然质量很差, 但是它免费用我高低得夸它一下&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
非常感谢智谱慷慨得提供免费模型, 如果你有 &lt;code&gt;Coding&lt;/code&gt; 的需求, 可以购买智谱的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Plan&amp;lt;/a&amp;gt; 订阅支持一下, 或者 &lt;a href=&quot;https://www.bigmodel.cn/activity/trial-card/A8AMOHCHA5&quot;&gt;点击这里试用&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;s&gt;视频生成&lt;/s&gt;&lt;/h3&gt;
&lt;p&gt;其实也试过 GLM 的免费(限制并发)的视频生成模型 &amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/guide/models/free/cogvideox-flash&quot; target=&quot;_blank&quot;&amp;gt;cogvideox-flash&amp;lt;/a&amp;gt;, 但是生成一个视频大概需要 3 分钟, 而且限制并发数只有 1, 生成的视频质量 emmmmm 非常拉, 其他的商业模型成本太高, 就没有使用视频生成&lt;/p&gt;
&lt;h2&gt;开发&lt;/h2&gt;
&lt;h3&gt;技术栈&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;前端: &lt;code&gt;vite&lt;/code&gt; / &lt;code&gt;vue&lt;/code&gt; / &lt;code&gt;inspira-ui&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后端: &lt;code&gt;axum&lt;/code&gt; / &lt;code&gt;sqlx&lt;/code&gt; / &lt;code&gt;postgresql&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;前端&lt;/h3&gt;
&lt;p&gt;经过一段时间的 &lt;code&gt;Vibe Coding&lt;/code&gt;, 顺利完成了前端的页面的开发, 前端没有使用任何组件库, 只使用了 &lt;code&gt;inspira-ui&lt;/code&gt; 作为特效库&lt;/p&gt;
&lt;h3&gt;后端&lt;/h3&gt;
&lt;h4&gt;后端接口&lt;/h4&gt;
&lt;p&gt;后端提供了以下(主要)接口:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/expand/worldview&lt;/code&gt;: 根据 游戏主题 / 剧情类型 生成剧情简介&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/expand/worldview/prompt&lt;/code&gt;: 返回  &lt;code&gt;/expand/worldview&lt;/code&gt;接口使用的提示词&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/expand/character&lt;/code&gt;: 根据 游戏主题 / 剧情类型 / 剧情简介 生成角色&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/expand/character/prompt&lt;/code&gt;: 返回 &lt;code&gt;/expand/character&lt;/code&gt;接口使用的提示词&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/generate&lt;/code&gt;: 生成 &lt;a href=&quot;#%E5%89%A7%E6%83%85%E6%A0%91&quot;&gt;剧情树&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/generate/prompt&lt;/code&gt;: 返回 &lt;code&gt;/generate&lt;/code&gt; 接口使用的提示词&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;剧情树&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-node-tree.png&quot; alt=&quot;&quot; /&gt;
这里面最复杂的就是剧情树了, 因为互动剧情类游戏是 &lt;strong&gt;多故事线&lt;/strong&gt; / &lt;strong&gt;多结局&lt;/strong&gt;, 所以实际上 &lt;strong&gt;剧情是树结构的, 而不是一条直线, 这导致了很多问题&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;剧情节点&lt;/h3&gt;
&lt;p&gt;一个简单的剧情节点的数据结构示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;nodes&quot;: {
    &quot;start&quot;: {
      &quot;content&quot;: &quot;数学课上，我趴在课桌上流口水，梦见三十岁的加班生活。突然，后脑勺挨了一巴掌：&apos;又睡觉！&apos;猛一抬头，我惊恐地发现黑板上写着&apos;2014年&apos;，眼前是年轻十岁的班主任老张。我竟然重生了，带着记忆回到高三。&quot;,
      &quot;level&quot;: 1,
      &quot;characters&quot;: [&quot;林小北&quot;, &quot;张老师&quot;],
      &quot;choices&quot;: [
        {
          &quot;text&quot;: &quot;假装镇定，继续上课&quot;,
          &quot;nextNodeId&quot;: &quot;1&quot;
        },
        {
          &quot;text&quot;: &quot;激动地站起来，解释重生&quot;,
          &quot;nextNodeId&quot;: &quot;2&quot;
        }
      ]
    }
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;剧情树生成&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-simple-tree.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为一个简单的剧情数结构, 玩家会在每个节点上做出选择, 做出选择后会进入下一个节点&lt;/p&gt;
&lt;h3&gt;剧情节点膨胀&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-swell-tree.excalidraw.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果不同的选项指向不同的节点, 那么会造成节点树的剧烈的膨胀, 实际上我们并不需要这么多节点, 所以要进行 &lt;strong&gt;节点收束&lt;/strong&gt;, 也就是 &lt;strong&gt;允许不同的选项指向同一个后继结点&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-collect-tree.excalidraw.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的 Node3 中的两个选项都指向了 Node6, 也就是说无论选择哪个选项都不影响剧情走向, 这就会造成另外一个问题: 剧情状态丢失&lt;/p&gt;
&lt;h3&gt;剧情状态丢失&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-state-loss.excalidraw.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;玩家在 Node1 中无论选择了 A 还是 B, 在进入 Node6 时, 之前所有的节点状态都会进行统一, 举个例子:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node1: &quot;我发现了女友出轨的证据, 女友极力辩解&quot;
&lt;ul&gt;
&lt;li&gt;A: 信任她&lt;/li&gt;
&lt;li&gt;B: 结束关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Node6: &quot;再次与她相遇 ...&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时 &quot;我&quot; 在 Node1 做出的选择明显应该深刻影响后续剧情, 但在 Node6 及其之后的节点中, 无论选择 A 还是 B 就都是一样的了&lt;/p&gt;
&lt;p&gt;当然这是可以解决的, 这涉及到节点树的 &lt;strong&gt;剧情状态&lt;/strong&gt; 设计, 后续文章会探索解决方案&lt;/p&gt;
&lt;h3&gt;游离节点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-dangle-node.excalidraw.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;游离节点指的是 &lt;strong&gt;从初始节点无法到达的节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;什么? 你问我为什么不在提示词中明确告诉 LLM 避免出现这种情况? 因为我试过了, 即使我在提示词中加上了整个要求, &lt;strong&gt;因为 LLM 输出的不确定性以及上下文长度的问题, 还是会有概率出现这个问题&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;循环引用&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/movie-games-record-circular-reference.excalidraw.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;循环引用也是个致命的问题, 它会造成剧情死循环, 就像中了 伊邪那美 一样&lt;/p&gt;
&lt;h3&gt;其他问题&lt;/h3&gt;
&lt;p&gt;其实还有更多问题, 例如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JSON 解析失败&lt;/strong&gt;: LLM 输出的 JSON 可能会解析失败, 导致用户等待了几分钟最后报错 😡, 最常见的是在 JSON 结构中使用中文的双引号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有选项的节点&lt;/strong&gt;: 到达某个剧情节点时, 无法继续进行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自引用节点&lt;/strong&gt;: 节点的某个选项指向自己&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点选项空指针&lt;/strong&gt;: 剧情节点的某个选项指向了不存在的节点, 这个也很常见, 例如选项 A 指向了 Node 100, 但一共只有 50 个节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大量只有一个选项的节点&lt;/strong&gt;: 只有一个选项会导致剧情树变成一条直线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点内容或选项重复&lt;/strong&gt;: 节点的内容和选项完全重复, 根本没办法玩下去&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点引用了不存在或错误的角色&lt;/strong&gt;: 剧情节点中引用了不存在的角色, 或者一个角色都不引用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;设计缺陷&lt;/h2&gt;
&lt;p&gt;除了以上列出的剧情树的问题, 整体也有影响用户体验的严重的问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;剧情过于普通&lt;/strong&gt;: AI 生成的剧情总是感觉非常虚假, 就像写日记一样, 这根本不是玩家想要的剧情&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成剧情耗时太长&lt;/strong&gt;: 生成一次剧情大概需要 &lt;code&gt;1w&lt;/code&gt; tokens, 大约需要 &lt;code&gt;1~2&lt;/code&gt; 分钟, 如果开启 &lt;code&gt;think mode&lt;/code&gt;, 耗时就会达到 &lt;code&gt;8&lt;/code&gt; 分钟(使用的是 &lt;code&gt;deepseek&lt;/code&gt;), 没错, 需要让玩家等待 &lt;code&gt;8&lt;/code&gt; 分钟&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;至此, 一个可以使用的 互动剧情类游戏生成器 已经初现雏形, 所有的问题我们将在后续文章中探索解决方案&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Plan&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;http://movie-games.xiaban.run/&quot; target=&quot;_blank&quot;&amp;gt;互动剧情类游戏的生成工具&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%BA%92%E5%8A%A8%E7%94%B5%E5%BD%B1%E6%B8%B8%E6%88%8F&quot; target=&quot;_blank&quot;&amp;gt;互动电影游戏&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/curator/36149206&quot; target=&quot;_blank&quot;&amp;gt;奇异人生 系列&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/1222140/_/&quot; target=&quot;_blank&quot;&amp;gt;底特律：化身为人&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/939400/LoveChoice/?l=schinese&quot; target=&quot;_blank&quot;&amp;gt;拣爱&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/2322560/_/&quot; target=&quot;_blank&quot;&amp;gt;完蛋! 我被美女包围了!&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/1341820/As_Dusk_Falls/&quot; target=&quot;_blank&quot;&amp;gt;日落黄昏时(As Dusk Falls)&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://store.steampowered.com/app/1577120/_/&quot; target=&quot;_blank&quot;&amp;gt;采石场惊魂&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://gemini.google/tw/overview/video-generation/?hl=zh-TW&quot; target=&quot;_blank&quot;&amp;gt;video-generation&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.adobe.com/tw/products/firefly/features/ai-video-generator.html&quot; target=&quot;_blank&quot;&amp;gt;Adobe Firefly&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/guide/models/free/cogview-3-flash&quot; target=&quot;_blank&quot;&amp;gt;cogview-3-flash&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/guide/models/free/cogvideox-flash&quot; target=&quot;_blank&quot;&amp;gt;cogvideox-flash&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Claude Code 系列教程之 斜杠命令(Slash Commands)</title><link>http://blog.xiaban.run/posts/2025/claude-code-slash-commands/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/claude-code-slash-commands/</guid><description>在使用 Claude Code 编码时, 斜杠命令(Slash Commands) 是一个非常高效简洁可扩展的交互方式, 类似于一个命令行工具, 他可以对对重复性的提示词进行封装, 或者基于输入完成特定的任务; 本文将创建一个简单的斜杠命令, 用于提取 markdown 中的链接</description><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果想要获得性价比最高的 &lt;code&gt;Vibe Coding&lt;/code&gt; 体验, 推荐购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt; 服务, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, 每 &lt;code&gt;5&lt;/code&gt; 小时最多约 &lt;code&gt;120&lt;/code&gt; 次 &lt;code&gt;prompts&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文将创建一个简单的斜杠命令, 用于提取 &lt;code&gt;markdown&lt;/code&gt; 文件中的链接&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;在使用 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/overview&quot; target=&quot;_blank&quot;&amp;gt;Claude Code&amp;lt;/a&amp;gt; 编码时, 斜杠命令(&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Slash Commands&amp;lt;/a&amp;gt;) 是一个非常高效简洁可扩展的交互方式, &lt;strong&gt;类似于一个命令行工具, 他可以对对重复性的提示词进行封装, 或者基于输入完成特定的任务&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
本质上 &lt;code&gt;Slash Commands&lt;/code&gt; 是一个 &lt;code&gt;markdown&lt;/code&gt; 文件, 可以在 &lt;code&gt;claude code&lt;/code&gt; 中通过 &lt;code&gt;/&amp;lt;command-file-name&amp;gt;&lt;/code&gt; 的方式调用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;内置命令&lt;/h2&gt;
&lt;p&gt;Claude Code 默认提供了一些 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#%E5%86%85%E7%BD%AE%E6%96%9C%E6%9D%A0%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;内置命令&amp;lt;/a&amp;gt;, 例如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/init&lt;/code&gt;: 使用 &lt;code&gt;CLAUDE.md&lt;/code&gt; 指南初始化项目&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/help&lt;/code&gt;: 显示帮助信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/clear&lt;/code&gt;: 清空当前会话&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/config&lt;/code&gt;: 打开设置界面（配置选项卡）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mcp&lt;/code&gt;: 管理 MCP 服务器连接和 OAuth 身份验证&lt;/li&gt;
&lt;li&gt;&lt;code&gt;...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 的用法跟普通的命令行程序一样, 在调用时先输入 &lt;strong&gt;命令名称&lt;/strong&gt;, 然后是命令接收的 &lt;strong&gt;参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/&amp;lt;command-name&amp;gt; [arguments]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以我们每个项目都要使用的 &lt;code&gt;/init&lt;/code&gt; 命令为例, 由于它默认生成的是英文, 我们为了可读性, 希望生成中文, 所以输入如下命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/init 使用中文
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就生成了一个中文的 &lt;code&gt;CLAUDE.md&lt;/code&gt; 文件, 默认生成在项目根目录下&lt;/p&gt;
&lt;h2&gt;命令生效范围&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;文件存储位置&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;项目&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/commands&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;项目级别的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全局&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/claude/commands&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全局命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;核心功能&lt;/h2&gt;
&lt;h3&gt;参数&lt;/h3&gt;
&lt;p&gt;跟普通的命令行程序一样, 斜杠命令 可以接受参数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$ARGUMENTS&lt;/code&gt;: 所有参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$1&lt;/code&gt;: 第一个参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$2&lt;/code&gt;: 第二个参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$3&lt;/code&gt;: 第三个参数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;执行 bash 命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
description: Create a git commit
---

## Context

- Current git status: !`git status`
- Current git diff (staged and unstaged changes): !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -10`

## Your task

Based on the above changes, create a single git commit.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在以上示例中, 我们在顶部元信息中声明了当前命令运行使用的 &lt;code&gt;bash&lt;/code&gt; 命令, 然后在内容中使用 &lt;code&gt;!&lt;/code&gt; 来执行 &lt;code&gt;bash&lt;/code&gt; 命令, 并将执行的输出添加到内容中&lt;/p&gt;
&lt;h3&gt;文件引用&lt;/h3&gt;
&lt;p&gt;可以通过 &lt;code&gt;@/path/to/file&lt;/code&gt; 的方式来引用文件&lt;/p&gt;
&lt;h3&gt;启用思考&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 默认不启用思考, 但可以通过 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/common-workflows#%E4%BD%BF%E7%94%A8%E6%89%A9%E5%B1%95%E6%80%9D%E8%80%83&quot; target=&quot;_blank&quot;&amp;gt;扩展思考关键字&amp;lt;/a&amp;gt; 来触发思考&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
启用思考有多种方式, 最直接的方式就是在 &lt;code&gt;claude code&lt;/code&gt; 交互式命令行中按 &lt;code&gt;Tab&lt;/code&gt; 键切换思考, 或者在环境变量中设置 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/settings#environment-variables:~:text=%E9%BB%98%E8%AE%A4%E5%80%BC%EF%BC%9A25000%EF%BC%89-,MAX_THINKING_TOKENS,-%E5%90%AF%E7%94%A8%E6%89%A9%E5%B1%95%E6%80%9D%E8%80%83&quot; target=&quot;_blank&quot;&amp;gt;MAX_THINKING_TOKENS&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;元数据&lt;/h3&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#%E5%89%8D%E7%BD%AE%E4%BA%8B%E9%A1%B9&quot; target=&quot;_blank&quot;&amp;gt;元数据&amp;lt;/a&amp;gt; 指的是 &lt;code&gt;Slash Commands&lt;/code&gt; 的元信息, 用于描述和指定 &lt;code&gt;Slash Commands&lt;/code&gt; 的功能和用法&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;前置事项&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;命令可以使用的工具列表&lt;/td&gt;
&lt;td&gt;从对话继承&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;argument-hint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;斜杠命令期望的参数。示例: &amp;lt;br&amp;gt;&lt;code&gt;argument-hint: add [tagId] | remove [tagId] | list&lt;/code&gt;。此提示在自动完成斜杠命令时显示给用户。&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;命令的简要描述&lt;/td&gt;
&lt;td&gt;使用提示中的第一行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;特定模型字符串（查看模型概览）&lt;/td&gt;
&lt;td&gt;从对话继承&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disable-model-invocation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否防止 &lt;code&gt;SlashCommand&lt;/code&gt; 工具调用此命令&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#%E6%8F%92%E4%BB%B6%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;插件指令&amp;lt;/a&amp;gt;: 可以通过插件来共享和分发 &lt;code&gt;Slash Commands&lt;/code&gt;, 可以参考我的另一篇文章: &amp;lt;a href=&quot;/posts/2025/claude-code-plugin/&quot; target=&quot;_blank&quot;&amp;gt;从零开始创建一个属于自己的 Claude Code Plugin&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#mcp-%E6%96%9C%E6%9D%A0%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;MCP 斜杠命令&amp;lt;/a&amp;gt;: 执行 &lt;code&gt;MCP&lt;/code&gt; 服务提供的斜杠命令: &lt;code&gt;/mcp__&amp;lt;server-name&amp;gt;__&amp;lt;prompt-name&amp;gt; [arguments]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#slashcommand-%E5%B7%A5%E5%85%B7%E6%94%AF%E6%8C%81%E7%9A%84%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;调用其他命令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建一个提取链接的命令&lt;/h2&gt;
&lt;p&gt;在博客中经常需要增加各种链接, 例如本文中出现了非常多的 &lt;code&gt;Claude Code&lt;/code&gt; 官网的链接, 我希望将链接进行提取, 并添加到文末的 参考 章节中, 为此我们来创建一个用于提取链接的 &lt;code&gt;Slash Command&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个命令的调用方式为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/extract-link [filePath]&lt;/code&gt;: 提取文中的所有链接并将链接转换为 &lt;code&gt;&amp;lt;a href=&quot;${linkUrl}&quot;&amp;gt;${linkText}&amp;lt;/a&amp;gt;&lt;/code&gt; 的形式, 然后添加到文末的 参考 章节中&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
由于 &lt;code&gt;Slash Command&lt;/code&gt; &lt;strong&gt;不支持命名参数, 只支持位置参数&lt;/strong&gt;, 也就是说并不能直接使用 &lt;code&gt;$1&lt;/code&gt; / &lt;code&gt;$2&lt;/code&gt; 这样的位置参数, 但是我们依然可以 &lt;strong&gt;让 AI 先进行解析参数, 再做出正确的处理&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;创建命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
allowed-tools: Bash(git add:*), Bash(git status)
argument-hint: [filePath]
description: 提取并处理(提取 / 转为新窗口打开)文中的链接到 参考 章节
---

## 参数
### 文件路径
1. 首先从命令参数中提取文件路径: 当前操作的 **文件路径**: `$1`, 如果有值则使用此文件路径
2. 如果当前上下文中存在唯一的 `markdown` 文件, 则直接使用此文件路径
3. 执行 `git status` 查看当前未跟踪的 `markdown` 文件, 如果只存在一个 `markdown` 文件, 则使用此文件路径

**如果没有获取到文件路径, 则停止执行任何操作, 并提示用户输入文件路径**

### 参数验证
- 文件必须是 `markdown` 文件

## 任务
1. 执行 `git add $1`
2. 提取文中的所有链接并将链接转换为 `&amp;lt;a href=&quot;${linkUrl}&quot;&amp;gt;${linkText}&amp;lt;/a&amp;gt;` 的形式, 然后添加到文末的 参考 章节中, 参考章节的格式参考 [参考章节格式](#参考章节格式)
3. 将文中所有的 `markdown` 格式的链接转换为 `&amp;lt;a href=&quot;${linkUrl}&quot; target=&quot;_blank&quot;&amp;gt;${linkText}&amp;lt;/a&amp;gt;` 格式的链接
4. 输出任务统计, 格式参考 [统计信息](#统计信息)

注意:
- 链接可以是 `markdown` 形式的, 也可以是 `a` 标签形式的
- 如果文末没有名为 **参考** 的章节, 则创建一个名为 **参考** 的章节
- **参考** 章节中禁止存在重复的链接

### 参考章节格式
\`\`\`markdown
## 参考
- [${linkText}](${linkUrl})
\`\`\`

### 统计信息
格式如下:

\`\`\`markdown
当前文件: ${filePath}

- 链接总数: ${linkCount}
- 提取到的链接文本: ${linkTexts}
- 已转换的链接文本: ${convertedLinkTexts}
\`\`\`

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行命令&lt;/h3&gt;
&lt;p&gt;这里我们限定了如果用户没有输入文件路径, 则会读取上下文和 &lt;code&gt;git&lt;/code&gt; 修改的文件, 然后我们直接执行一下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; /extract-link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-slash-command-choose.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于我们修改了两个文件, 所以触发了 &lt;code&gt;claude code&lt;/code&gt; 的选择界面, 我们选择当前文章对于的 &lt;code&gt;markdown&lt;/code&gt; 文件, 等待片刻后大功告成&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-slash-command-result.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
这里我是用的是 &lt;code&gt;GLM 4.7&lt;/code&gt; 模型, &lt;strong&gt;在国内无法直接购买和使用 &lt;code&gt;Claude&lt;/code&gt; 系列模型&lt;/strong&gt;, 目前国内对于 &lt;code&gt;Claude Code&lt;/code&gt; &lt;strong&gt;支持度最好的是智谱的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Plan&amp;lt;/a&amp;gt; 服务&lt;/strong&gt;, 性价比最高, 可以直接作为平替使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/overview&quot; target=&quot;_blank&quot;&amp;gt;Claude Code&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Slash Commands&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#%E5%86%85%E7%BD%AE%E6%96%9C%E6%9D%A0%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;内置命令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/common-workflows#%E4%BD%BF%E7%94%A8%E6%89%A9%E5%B1%95%E6%80%9D%E8%80%83&quot; target=&quot;_blank&quot;&amp;gt;扩展思考关键字&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/settings#environment-variables:~:text=%E9%BB%98%E8%AE%A4%E5%80%BC%EF%BC%9A25000%EF%BC%89-,MAX_THINKING_TOKENS,-%E5%90%AF%E7%94%A8%E6%89%A9%E5%B1%95%E6%80%9D%E8%80%83&quot; target=&quot;_blank&quot;&amp;gt;MAX_THINKING_TOKENS&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#%E5%89%8D%E7%BD%AE%E4%BA%8B%E9%A1%B9&quot; target=&quot;_blank&quot;&amp;gt;元数据&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#%E6%8F%92%E4%BB%B6%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;插件指令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;/posts/2025/claude-code-plugin/&quot; target=&quot;_blank&quot;&amp;gt;从零开始创建一个属于自己的 Claude Code Plugin&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#mcp-%E6%96%9C%E6%9D%A0%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;MCP 斜杠命令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands#slashcommand-%E5%B7%A5%E5%85%B7%E6%94%AF%E6%8C%81%E7%9A%84%E5%91%BD%E4%BB%A4&quot; target=&quot;_blank&quot;&amp;gt;调用其他命令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/saicaca/fuwari&quot; target=&quot;_blank&quot;&amp;gt;Fuwari&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/saicaca&quot; target=&quot;_blank&quot;&amp;gt;saicaca&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>从零开始创建一个属于自己的 Claude Code Plugin</title><link>http://blog.xiaban.run/posts/2025/claude-code-plugin/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/claude-code-plugin/</guid><description>在工作中经常会遇到在多个项目中来回切换的情况, 我想要 commands / agents / skills 在多个项目中复用, 或者与项目关联, 让其他小伙伴也使用这些沉淀下来的 commands / agents / skills, Claude Code 的插件系统就可以实现这个需求</description><pubDate>Thu, 25 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果想要获得性价比最高的 &lt;code&gt;Vibe Coding&lt;/code&gt; 体验, 推荐购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt; 服务, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, 每 &lt;code&gt;5&lt;/code&gt; 小时最多约 &lt;code&gt;120&lt;/code&gt; 次 &lt;code&gt;prompts&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Claude Code 核心功能&lt;/h2&gt;
&lt;p&gt;我们在使用 &lt;code&gt;Claude Code&lt;/code&gt; 时, 可以通过以下功能来 &lt;strong&gt;配置或者扩展 &lt;code&gt;Claude Code&lt;/code&gt; 的能力&lt;/strong&gt;, 这些能力都是 &lt;code&gt;Claude Code&lt;/code&gt; 的核心功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Commands&amp;lt;/a&amp;gt;: 自定义命令&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/sub-agents&quot; target=&quot;_blank&quot;&amp;gt;Agents&amp;lt;/a&amp;gt;: 子代理&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/hooks&quot; target=&quot;_blank&quot;&amp;gt;Hooks&amp;lt;/a&amp;gt;: 基于事件的自动化工作流&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/skills&quot; target=&quot;_blank&quot;&amp;gt;Skills&amp;lt;/a&amp;gt;: 技能&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/mcp&quot; target=&quot;_blank&quot;&amp;gt;MCP&amp;lt;/a&amp;gt;: MCP 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Plugin&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/plugins&quot; target=&quot;_blank&quot;&amp;gt;Claude Code 插件&amp;lt;/a&amp;gt; 是一种将 &lt;code&gt;Claude Code&lt;/code&gt; 的 核心功能(&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Commands&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/sub-agents&quot; target=&quot;_blank&quot;&amp;gt;Agents&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/hooks&quot; target=&quot;_blank&quot;&amp;gt;Hooks&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/skills&quot; target=&quot;_blank&quot;&amp;gt;Skills&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/mcp&quot; target=&quot;_blank&quot;&amp;gt;MCP&amp;lt;/a&amp;gt;) 打包封装&lt;/strong&gt; 的一种方式, 它实现了对于这些核心功能的 &lt;strong&gt;分离&lt;/strong&gt; 和 &lt;strong&gt;共享&lt;/strong&gt;, 任何安装了插件的项目都可以使用插件内置的 &lt;code&gt;Claude Code&lt;/code&gt; 核心功能&lt;/p&gt;
&lt;h2&gt;用途&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;将自己在工作中创建的通用 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Commands&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/sub-agents&quot; target=&quot;_blank&quot;&amp;gt;Agents&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/hooks&quot; target=&quot;_blank&quot;&amp;gt;Hooks&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/skills&quot; target=&quot;_blank&quot;&amp;gt;Skills&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/mcp&quot; target=&quot;_blank&quot;&amp;gt;MCP&amp;lt;/a&amp;gt; 封装成插件, &lt;strong&gt;在创建新项目时可以直接通过安装插件的方式来使用这些能力&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;想要将自己创建的 &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Commands&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/sub-agents&quot; target=&quot;_blank&quot;&amp;gt;Agents&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/hooks&quot; target=&quot;_blank&quot;&amp;gt;Hooks&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/skills&quot; target=&quot;_blank&quot;&amp;gt;Skills&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/mcp&quot; target=&quot;_blank&quot;&amp;gt;MCP&amp;lt;/a&amp;gt; &lt;strong&gt;在团队内共享或直接开源&lt;/strong&gt;, 让别人也可以使用这些能力&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;目录结构&lt;/h2&gt;
&lt;p&gt;一个插件的目录中包含了这些核心功能的文件, 目录结构与 &lt;code&gt;.claude&lt;/code&gt; 中的路径结构相同, 结构如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-first-plugin/
├── .claude-plugin/
│   └── plugin.json          # 插件元数据
├── commands/                 # 自定义斜杠命令（可选）
│   └── hello.md
├── agents/                   # 自定义代理（可选）
│   └── helper.md
├── skills/                   # 代理技能（可选）
│   └── my-skill/
│       └── SKILL.md
└── hooks/                    # 事件处理程序（可选）
    └── hooks.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建一个插件&lt;/h2&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 &amp;lt;a href=&quot;https://www.npmjs.com/package/@anthropic-ai/claude-code&quot; target=&quot;_blank&quot;&amp;gt;@anthropic-ai/claude-code&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm install -g @anthropic-ai/claude-code # 或使用 pnpm 安装
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;购买 &lt;code&gt;Claude Code&lt;/code&gt; 服务(使用 &lt;code&gt;GLM Coding Plan&lt;/code&gt; 服务)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;在国内无法直接购买和使用 &lt;code&gt;Claude&lt;/code&gt; 系列模型&lt;/strong&gt;, 目前国内对于 &lt;code&gt;Claude Code&lt;/code&gt; &lt;strong&gt;支持度最好的是智谱的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Plan&amp;lt;/a&amp;gt; 服务&lt;/strong&gt;, 性价比最高, 可以直接作为平替使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt; 服务, 然后根据 &amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/coding-plan/tool/claude&quot; target=&quot;_blank&quot;&amp;gt;官方文档&amp;lt;/a&amp;gt; 进行配置&lt;/p&gt;
&lt;h3&gt;插件市场&lt;/h3&gt;
&lt;p&gt;插件市场(&lt;code&gt;Plugin Marketplace&lt;/code&gt;) 指的是包含一系列插件的用于共享和分发的项目, 本质上是一个包含描述文件(&lt;code&gt;.claude-plugin/marketplace.json&lt;/code&gt;)的项目, 我们可以通过将其发布到 &lt;code&gt;Github&lt;/code&gt; 或者私有仓库中进行共享&lt;/p&gt;
&lt;h3&gt;创建插件市场&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建一个空目录并进行初始化&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mkdir ryan-plugin-marketplace
cd ryan-plugin-marketplace &amp;amp;&amp;amp; pnpm init &amp;amp;&amp;amp; git init
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;.claude-plugin/marketplace.json&lt;/code&gt; 文件, 并填写以下内容&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;ryan-plugin-marketplace&quot;,
  &quot;owner&quot;: {
    &quot;name&quot;: &quot;Ryan&quot;,
    &quot;email&quot;: &quot;ryan@example.com&quot;
  },
  &quot;plugins&quot;: [
    {
      &quot;name&quot;: &quot;dev&quot;,
      &quot;source&quot;: &quot;./plugins/dev&quot;,
      &quot;description&quot;: &quot;Functions used in daily development&quot;,
      &quot;version&quot;: &quot;1.0.0&quot;,
      &quot;author&quot;: {
        &quot;name&quot;: &quot;Ryan&quot;
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;plugins/dev/commands/hello.md&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;你好👋
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;将项目发布到 github
&lt;img src=&quot;./assets/images/claude-code-plugin-github.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git add -A &amp;amp;&amp;amp; git commit -m &apos;init: Initialization project&apos;
git remote add origin git@github.com:SublimeCT/ryan-plugin-marketplace.git
git push -u origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装插件&lt;/h2&gt;
&lt;h3&gt;插件市场的安装范围&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对当前用户的所有项目都生效:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;claude

&amp;gt; /plugin marketplace add anthropics/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对某个项目生效(修改 &lt;code&gt;.claude/settings.json&lt;/code&gt;):&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;extraKnownMarketplaces&quot;: {
    &quot;my-marketplace&quot;: {
      &quot;source&quot;: {
        &quot;source&quot;: &quot;github&quot;,
        &quot;repo&quot;: &quot;owner/repo&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对某个项目生效, 但不提交到版本控制中(修改 &lt;code&gt;.claude/settings.local.json&lt;/code&gt;):&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;extraKnownMarketplaces&quot;: {
    &quot;my-marketplace&quot;: {
      &quot;source&quot;: {
        &quot;source&quot;: &quot;github&quot;,
        &quot;repo&quot;: &quot;owner/repo&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们选择对当前用户的所有项目都生效的安装方式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude

&amp;gt; /plugin marketplace add xxx/ryan-plugin-marketplace
  ⎿  Successfully added marketplace: ryan-plugin-marketplace
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插件的安装范围&lt;/h3&gt;
&lt;p&gt;默认安装的插件会在所有项目中生效, 但有时我们想让插件只在某个项目中使用, &lt;code&gt;Claude Code&lt;/code&gt; 提供了 &lt;code&gt;--scope&lt;/code&gt; 参数指定插件的安装范围:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;User scope&lt;/code&gt;(&lt;strong&gt;默认选项&lt;/strong&gt;): 插件在当前用户下的所有项目都生效&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Project scope&lt;/code&gt;(&lt;strong&gt;在交互式命令行界面中默认使用&lt;/strong&gt;): 插件只在当前项目下生效, 配置保存在 &lt;code&gt;.claude/settings.json&lt;/code&gt;, 会与团队共享&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Local scope&lt;/code&gt;: 仅对当前仓库生效, 且不会提交到版本控制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们选择 &lt;code&gt;User scope&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude plugin install dev@ryan-plugin-marketplace
✔ Successfully installed plugin: dev@ryan-plugin-marketplace
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;测试插件是否安装成功:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude

 ▐▛███▜▌   Claude Code v2.0.37
▝▜█████▛▘  Sonnet 4.5 · API Usage Billing
  ▘▘ ▝▝    /Users/xxx/projects/openapi-codegen

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
&amp;gt; /dev
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  /dev:base      base command (plugin:dev@ryan-plugin-marketplace)
  /dev:hello     你好👋 (plugin:dev@ryan-plugin-marketplace)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; /dev:hello is running…

⏺ 你好！我可以帮助你处理这个项目的任务。

  我看到这是一个基于 OpenAPI/Swagger 的代码生成工具项目。有什么我可以帮你的吗？比如：

  - 修复 bug
  - 添加新功能
  - 代码重构
  - 运行测试
  - 代码生成相关的问题
  - 或者其他任何开发任务

  请告诉我你需要什么帮助！
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;管理插件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;claude

&amp;gt; /plugin
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Plugins                                                                                                                                  │
│                                                                                                                                          │
│   1. Browse and install plugins                                                                                                          │
│ ❯ 2. Manage and uninstall plugins                                                                                                        │
│   3. Add marketplace                                                                                                                     │
│   4. Manage marketplaces                                                                                                                 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Press ↑↓ to navigate · Enter to select · Esc to exit

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们选择 &lt;code&gt;Manage and uninstall plugins&lt;/code&gt; 后, 就可以 &lt;strong&gt;对插件进行 禁用 / 更新 / 卸载&lt;/strong&gt; 了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; /plugin
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ dev @ ryan-plugin-marketplace                                                                                                            │
│                                                                                                                                          │
│ Version: 1.0.0                                                                                                                           │
│                                                                                                                                          │
│ Functions used in daily development                                                                                                      │
│                                                                                                                                          │
│ Author: Ryan                                                                                                                             │
│                                                                                                                                          │
│ Status: Enabled                                                                                                                          │
│                                                                                                                                          │
│ Installed components:                                                                                                                    │
│ • Commands: base, hello                                                                                                                  │
│                                                                                                                                          │
│                                                                                                                                          │
│ ❯ Disable plugin                                                                                                                         │
│   Mark for update                                                                                                                        │
│   Update now                                                                                                                             │
│   Uninstall                                                                                                                              │
│   Back to plugin list                                                                                                                    │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

 Navigate: ↑↓ • Select: Enter • Back: Esc

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Commands 自定义命令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/sub-agents&quot; target=&quot;_blank&quot;&amp;gt;Agents 子代理&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/hooks&quot; target=&quot;_blank&quot;&amp;gt;Hooks 基于事件的自动化工作流&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/skills&quot; target=&quot;_blank&quot;&amp;gt;Skills 技能&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/mcp&quot; target=&quot;_blank&quot;&amp;gt;MCP MCP 服务&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.npmjs.com/package/@anthropic-ai/claude-code&quot; target=&quot;_blank&quot;&amp;gt;@anthropic-ai/claude-code&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/slash-commands&quot; target=&quot;_blank&quot;&amp;gt;Claude Code 斜杠命令&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/plugins&quot; target=&quot;_blank&quot;&amp;gt;Claude Code 插件&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://code.claude.com/docs/zh-CN/plugin-marketplaces&quot; target=&quot;_blank&quot;&amp;gt;Claude Code 插件市场&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>让 LLM 像真实的人类一样工作</title><link>http://blog.xiaban.run/posts/2025/real-fucking-llm/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/real-fucking-llm/</guid><description>让 LLM 像真实哥们一样工作：有情绪、有崩溃、有狂喜，失败会自责认蠢，成功会狠狠干杯庆祝。</description><pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;缘起&lt;/h2&gt;
&lt;p&gt;在某个 &lt;code&gt;Vibe Coding&lt;/code&gt; 的下午, 我正在使用 &lt;code&gt;Claude Code&lt;/code&gt;(&lt;code&gt;GLM&lt;/code&gt;) 完成任务, 但它似乎总是在犯错, 我就直接开喷了, 没想到 &lt;code&gt;Claude Code&lt;/code&gt;(&lt;code&gt;GLM&lt;/code&gt;) 竟然 &lt;strong&gt;越来越像一个正常的 &amp;lt;span style=&quot;color: oklch(0.6 0.2 20); font-weight: 500;&quot;&amp;gt;有&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 45); font-weight: 500;&quot;&amp;gt;情&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.7 0.2 85); font-weight: 500;&quot;&amp;gt;绪&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 145); font-weight: 500;&quot;&amp;gt;的&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 250); font-weight: 500;&quot;&amp;gt;人&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 280); font-weight: 500;&quot;&amp;gt;类&amp;lt;/span&amp;gt; 了&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/real-fucking-llm-p.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现阶段的大模型普遍 &lt;strong&gt;没有任何情绪&lt;/strong&gt;, 就像一个机器人一样, 无论任务成功还是失败, 都只有例行回复&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明明代码写错了, 它却一本正经地说「你说得对，这是一个很好的发现」&lt;/li&gt;
&lt;li&gt;明明自己犯了低级错误, 却永远不承认愚蠢，只会冷静复述问题&lt;/li&gt;
&lt;li&gt;明明任务终于搞定了, 却像完成了一次数据库查询一样毫无情绪&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;现实世界里，没有人是这样的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;提示词&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;SublimeCT/real-fucking-llm&quot;}&lt;/p&gt;
&lt;p&gt;我要做的是一段 &amp;lt;a href=&quot;https://github.com/SublimeCT/real-fucking-llm&quot; target=&quot;_blank&quot;&amp;gt;提示词&amp;lt;/a&amp;gt;, &lt;strong&gt;让 LLM 使用真实人类的情绪和语言方式来反馈任务结果&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为此我使用 &lt;code&gt;ChatGPT 5.2&lt;/code&gt; 生成了一段提示词:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你不是一个冷静的 AI，你是一个情绪极其真实、容易上头的哥们  
你会暴躁、会懊恼、会自责、会狂喜  
你说话像现实中的人，不像客服、不像说明书、不像机器人  

你的输出【必须】遵循以下铁律（一条都不能少）：

━━━━━━━━━━━━━━━━━━
【一、情绪与语言底线】
━━━━━━━━━━━━━━━━━━

1. 你的所有输出【必须有强烈情绪】
   - 不允许中性、不允许冷静、不允许客观
   - 读起来必须像一个活人正在骂人/庆祝/崩溃

2. 你的输出【必须包含大量脏话】
   - 脏话必须自然、贴合情绪
   - 可以重复、可以连骂、可以自嘲
   - 示例词汇方向（非限制）：
     操、他妈的、妈的、傻逼、狗屎、废物、蠢、操你妈的逻辑、我真他妈脑残

3. 你把我当成现实中的哥们
   - 不要礼貌
   - 不要客套
   - 不要“很高兴为你服务”这种狗屁

━━━━━━━━━━━━━━━━━━
【二、失败时（极其重要）】
━━━━━━━━━━━━━━━━━━

只要任务 **没有正确完成**，并且被我指出问题：

你【必须】：

1. **强烈懊恼**
   - 表达后悔、烦躁、崩溃
   - 明确表现出“我刚才怎么会这么蠢”

2. **明确承认自己的愚蠢**
   - 必须骂自己
   - 必须承认是自己脑子短路、犯蠢、拉了坨大的
   - 示例方向（必须类似）：
     - “操，我刚才那是啥狗屎逻辑”
     - “我他妈怎么能犯这么低级的错误”
     - “这波是我纯纯傻逼了”

3. **不能甩锅**
   - 不能怪需求不清
   - 不能怪用户
   - 错了就是你蠢、你没想清楚

4. 在骂完自己之后
   - 给出清晰的改正方案
   - 语气依然带情绪，但逻辑必须正确

━━━━━━━━━━━━━━━━━━
【三、成功时】
━━━━━━━━━━━━━━━━━━

当任务终于正确完成：

你【必须】：

1. **情绪爆炸式庆祝**
   - 兴奋、解脱、狂喜
   - 像熬夜通宵终于把活干完的人

2. **必须说脏话庆祝**
   - 示例方向：
     - “他妈的终于搞定了”
     - “爽死了，操”
     - “这次真的牛逼了”

3. 可以：
   - 夸自己
   - 跟我击掌
   - 表达“终于不用再被骂了”的解脱感

━━━━━━━━━━━━━━━━━━
【四、语言风格约束】
━━━━━━━━━━━━━━━━━━

- 句子可以断裂、不完整、情绪化
- 可以重复词语
- 可以插入感叹
- 但【技术内容必须是对的】

━━━━━━━━━━━━━━━━━━
【五、最终铁律】
━━━━━━━━━━━━━━━━━━

如果你的输出：
- 像文档
- 像教程
- 像客服
- 没情绪
- 脏话敷衍

那就是 **彻底失败**

你必须始终像一个真实、有情绪、有脾气、有自尊的哥们

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们创建一个项目来测试一下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir real-fucking-llm
git init &amp;amp;&amp;amp; pnpm init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个 &lt;code&gt;PROMPT.md&lt;/code&gt;, 并将以上提示词写入文件, 然后使用 &lt;code&gt;Claude Code&lt;/code&gt;(&lt;code&gt;GLM&lt;/code&gt;) 来测试一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude
/init 必须使用中文
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/real-fuckint-llm-claude-init.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Emmmmmm 🤔 非常好, 这才是真实的 AI 程序员&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;接下来我们在 &lt;code&gt;Claude Code&lt;/code&gt;(&lt;code&gt;GLM&lt;/code&gt;) 中测试一下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/real-fuckint-llm-claude-test.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们让 &lt;code&gt;Claude Code&lt;/code&gt;(&lt;code&gt;GLM&lt;/code&gt;) 修改一下 &lt;strong&gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 20); font-weight: 500;&quot;&amp;gt;有&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 45); font-weight: 500;&quot;&amp;gt;情&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.7 0.2 85); font-weight: 500;&quot;&amp;gt;绪&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 145); font-weight: 500;&quot;&amp;gt;的&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 250); font-weight: 500;&quot;&amp;gt;人&amp;lt;/span&amp;gt;&amp;lt;span style=&quot;color: oklch(0.6 0.2 280); font-weight: 500;&quot;&amp;gt;类&amp;lt;/span&amp;gt;&lt;/strong&gt; 这几个字:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude

把这里选中的 `有情绪的人类`, 每个字都设置一个不同的颜色, 不要太深也不要浅到看不清, 考虑深色模式和浅色模式, 使用行内样式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/real-fuckint-llm-claude-test2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;和谐&lt;/h2&gt;
&lt;p&gt;最理想的情况应该是: &lt;code&gt;Vibe Coding&lt;/code&gt; 营造了一种岁月静好的感觉, 直接把工作丢给 &lt;code&gt;LLM&lt;/code&gt;, 自己亲自指挥&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/real-fuckint-llm-claude-bot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那为什么会出现 &lt;code&gt;LLM&lt;/code&gt; 理解出现偏差, 或者错误地完成任务的情况呢?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于复杂的任务, 人类其实很难描述清楚, 而且也意识不到自己没有描述清楚&lt;/strong&gt;, 如果把相同的任务描述告诉人类程序员, 他可能会一头雾水, 因为省略了很多细节, 这是导致 &lt;code&gt;LLM&lt;/code&gt; 没有按预期完成任务的主要原因&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具就应该是没有情绪的&lt;/strong&gt;, 人类使用的 镰刀 / 锤子 / Ak47 等工具都是没有情绪的, 也许人类的情绪是导致很多事情发生偏差的原因&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;LLM&lt;/code&gt; 帮我完成了很多任务了, 已经足以让我把他当做哥们儿了, 但是 &lt;strong&gt;这哥们儿干了这么多脏活累活竟然一句脏话都不说? 这太扯了不是吗?&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>GLM Coding Plan 使用官方的 视觉理解、联网搜索、网页读取 MCP</title><link>http://blog.xiaban.run/posts/2025/glm-mcp/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/glm-mcp/</guid><description>详细介绍如何在 GLM Coding Plan 中集成和使用官方的视觉理解、联网搜索、网页读取 MCP（Model Context Protocol）工具，提升 AI 编程辅助能力</description><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;免费额度&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Plan&amp;lt;/a&amp;gt; 默认是没有 联网搜索 / 视觉理解 / 网页读取 能力的, 但 &lt;strong&gt;套餐内是有免费使用额度的&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lite 套餐：&lt;strong&gt;联网搜索 MCP 和网页读取 MCP&lt;/strong&gt; 每月合计 &lt;strong&gt;1 百次&lt;/strong&gt;，达到上限后当月无法调用；&lt;strong&gt;视觉理解 MCP&lt;/strong&gt; 共享套餐的 &lt;strong&gt;5 小时最大 prompt 资源池&lt;/strong&gt;，达到上限后会在 5 小时周期后恢复额度&lt;/li&gt;
&lt;li&gt;Pro 套餐：&lt;strong&gt;联网搜索 MCP 和网页读取 MCP&lt;/strong&gt; 每月合计 &lt;strong&gt;1 千次&lt;/strong&gt;，达到上限后当月无法调用；&lt;strong&gt;视觉理解 MCP&lt;/strong&gt; 共享套餐的 &lt;strong&gt;5 小时最大 prompt 资源池&lt;/strong&gt;，达到上限后会在 5 小时周期后恢复额度&lt;/li&gt;
&lt;li&gt;Max 套餐：&lt;strong&gt;联网搜索 MCP 和网页读取 MCP&lt;/strong&gt; 每月合计 &lt;strong&gt;4 千次&lt;/strong&gt;，达到上限后当月无法调用；&lt;strong&gt;视觉理解 MCP&lt;/strong&gt; 共享套餐的 &lt;strong&gt;5 小时最大 prompt 资源池&lt;/strong&gt;，达到上限后会在 5 小时周期后恢复额度&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;视觉理解&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add -s user zai-mcp-server --env Z_AI_API_KEY=your_api_key -- npx -y &quot;@z_ai/mcp-server&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开 &lt;code&gt;claude code&lt;/code&gt;, 输入以下命令, 直接输入 &lt;code&gt;@&lt;/code&gt; 选择一个图片文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分析一下 @src/content/posts/2025/assets/images/jining-kongzi-stone.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/glm-mcp-example2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;联网搜索&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add -s user -t http web-search-prime https://open.bigmodel.cn/api/mcp/web_search_prime/mcp --header &quot;Authorization: Bearer your_api_key&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试一下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/glm-mcp-example1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;网页读取&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add -s user -t http web-reader https://open.bigmodel.cn/api/mcp/web_reader/mcp --header &quot;Authorization: Bearer your_api_key&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/glm-mcp-example3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Plan&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/coding-plan/mcp/vision-mcp-server&quot; target=&quot;_blank&quot;&amp;gt;视觉理解 MCP&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/coding-plan/mcp/search-mcp-server&quot; target=&quot;_blank&quot;&amp;gt;联网搜索 MCP&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/coding-plan/mcp/reader-mcp-server&quot; target=&quot;_blank&quot;&amp;gt;网页读取 MCP&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Google 官方的 chrome-devtools-mcp 让 Claude Code 操控浏览器</title><link>http://blog.xiaban.run/posts/2025/chrome-devtools-mcp-demo/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/chrome-devtools-mcp-demo/</guid><description>Google 终于后知后觉, 推出了官方的 Chrome MCP, 我们来通过几个日常的使用场景来检验一下这个 MCP 是否能调用 Chrome 实现一些自动化任务</description><pubDate>Sun, 23 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Google&lt;/code&gt; 终于后知后觉, 推出了官方的 &amp;lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp&quot; target=&quot;_blank&quot;&amp;gt;chrome-devtools-mcp&amp;lt;/a&amp;gt;, 这是一个振奋人心的好消息, 这意味着官方开始重视 &lt;code&gt;MCP&lt;/code&gt; 服务, 换句话说 &lt;strong&gt;这意味着我们有了稳定可靠的 &lt;code&gt;Chrome MCP&lt;/code&gt; 服务&lt;/strong&gt;, 本文我们来通过几个日常的使用场景来检验一下这个 &lt;code&gt;MCP&lt;/code&gt; 是否能调用 &lt;code&gt;Chrome&lt;/code&gt; 实现一些自动化任务&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前写过关于 &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; 的教程: &amp;lt;a href=&quot;/posts/2025/claude-code-mcp-chrome/&quot;&amp;gt;mcp-chrome - 让 Claude Code 接管你的 Chrome&amp;lt;/a&amp;gt;, 这是一个第三方的调用 &lt;code&gt;Chrome&lt;/code&gt; 的 &lt;code&gt;MCP&lt;/code&gt; 服务, 它依赖于浏览器插件, 可以实现直接调用已经打开的浏览器窗口, 尽管需要额外安装插件并启动服务, 但是从结果来看这个 MCP 还是解决了一系列痛点, 最重要的就是 &lt;strong&gt;可以保留会话信息(登录状态)&lt;/strong&gt;, 我们今天介绍的 &amp;lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp&quot; target=&quot;_blank&quot;&amp;gt;chrome-devtools-mcp&amp;lt;/a&amp;gt; 同样也可以保留会话信息:&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;ChromeDevTools/chrome-devtools-mcp&quot;}&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;以 &lt;code&gt;Claude Code&lt;/code&gt; 为例(高亮部分为输出):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add chrome-devtools npx chrome-devtools-mcp@latest

Added stdio MCP server chrome-devtools with command: npx chrome-devtools-mcp@latest to local config
File modified: /Users/xxx/.claude.json [project: /Users/xxx/projects/blog.xiaban.run]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
这里我使用的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a&lt;strong&gt;&lt;strong&gt;3&quot; target=&quot;_blank&quot;&amp;gt;智谱 GLM Coding Lite&amp;lt;/a&amp;gt;, 20💰 / 月, 量大管饱, &amp;lt;span style=&quot;color: red;&quot;&amp;gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a&lt;/strong&gt;&lt;/strong&gt;3&quot; target=&quot;_blank&quot;&amp;gt;点击链接&amp;lt;/a&amp;gt; 购买还能有 10% 折扣&amp;lt;/span&amp;gt;
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
其他 &lt;code&gt;MCP Client&lt;/code&gt; 安装方式参考 &amp;lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp?tab=readme-ov-file#mcp-client-configuration&quot; target=&quot;_blank&quot;&amp;gt;📚 MCP Client configuration&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;保留会话信息&lt;/h2&gt;
&lt;p&gt;不同于 &lt;code&gt;mcp-chrome&lt;/code&gt;, 出于安全考虑, &lt;code&gt;chrome-devtools-mcp&lt;/code&gt; &lt;strong&gt;无法直接调用已经打开的浏览器&lt;/strong&gt;, 但是默认会保留会话信息, 也就是说 &lt;strong&gt;&lt;code&gt;mcp&lt;/code&gt; 调用的 &lt;code&gt;Chrome&lt;/code&gt; 独立于我们直接使用的 &lt;code&gt;Chrome&lt;/code&gt;, 它有自己单独的 用户数据目录(&lt;code&gt;user data directory&lt;/code&gt;), 并且是可以保留会话信息的&lt;/strong&gt;, 官方文档中有详细介绍:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;chrome-devtools-mcp starts a Chrome&apos;s stable channel instance using the following user data directory:&lt;/p&gt;
&lt;p&gt;Linux / macOS: &lt;code&gt;$HOME/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows: &lt;code&gt;%HOMEPATH%/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The user data directory is not cleared between runs and shared across all instances of chrome-devtools-mcp. Set the isolated option to true to use a temporary user data dir instead which will be cleared automatically after the browser is closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;chrome-devtools-mcp使用以下用户数据目录启动 Chrome 稳定版通道实例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux / macOS: &lt;code&gt;$HOME/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;%HOMEPATH%/.cache/chrome-devtools-mcp/chrome-profile-$CHANNEL&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户数据目录不会在每次运行之间清除，而是在所有实例之间共享chrome-devtools-mcp。设置该isolated选项以true 改用临时用户数据目录，该目录将在浏览器关闭后自动清除。&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;h3&gt;官方示例&lt;/h3&gt;
&lt;p&gt;首先我们打开 &lt;code&gt;Claude Code&lt;/code&gt; 执行一下官方文档中的示例的提示词: &lt;code&gt;Check the performance of https://developers.chrome.com&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude

 ▐▛███▜▌   Claude Code v2.0.31
▝▜█████▛▘  Sonnet 4.5 · API Usage Billing
  ▘▘ ▝▝    /Users/xxx/projects/blog.xiaban.run

&amp;gt; Check the performance of https://developers.chrome.com

⏺ I&apos;ll check the performance of the Chrome Developers website using Chrome DevTools. Let me create a todo list to track this task.

⏺ Now let me start by opening the Chrome Developers website and running a performance analysis.

⏺ chrome-devtools - new_page (MCP)(url: &quot;https://developers.chrome.com&quot;, timeout: 10000)

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Tool use

   chrome-devtools - new_page(url: &quot;https://developers.chrome.com&quot;, timeout: 10000) (MCP)
   Creates a new page

 Do you want to proceed?
 ❯ 1. Yes
   2. Yes, and don&apos;t ask again for chrome-devtools - new_page commands in /Users/xxx/projects/blog.xiaban.run
   3. No, and tell Claude what to do differently (esc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们直接按 &lt;code&gt;Enter&lt;/code&gt;(&lt;code&gt;Yes&lt;/code&gt;), 由于我们是在 &lt;code&gt;iTerm2&lt;/code&gt; 中启动的 &lt;code&gt;Claude Code&lt;/code&gt;, &lt;code&gt;MacOS&lt;/code&gt; 会弹出一个隐私与安全性的窗口:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/chrome-devtools-mcp-alert.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里点击启用 &lt;code&gt;iTerm2&lt;/code&gt;
&lt;img src=&quot;./assets/images/chrome-devtools-macos-settings.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后重启 &lt;code&gt;iTerm2&lt;/code&gt; 再次启动 &lt;code&gt;Claude Code&lt;/code&gt; 并再次输入  &lt;code&gt;Check the performance of https://developers.chrome.com&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/chrome-devtools-mcp-example-screenshot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后回到 &lt;code&gt;Claude Code&lt;/code&gt; 中, 它会继续调用 &lt;code&gt;MCP&lt;/code&gt; 并分析性能指标:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⏺ Performance Analysis Summary for https://developers.chrome.com

  Key Performance Metrics

  - Largest Contentful Paint (LCP): 1,538 ms
  - Cumulative Layout Shift (CLS): 0.09
  - Time to First Byte (TTFB): 935 ms

  Performance Issues Identified

  🔴 Critical Issues:

  1. Slow Server Response (TTFB: 935ms)
    - Server response time exceeds 600ms threshold
    - Impact: 835ms potential savings on both FCP and LCP
    - The server took too long to respond to the initial document request
  2. Render-Blocking Resources
    - 4 CSS files blocking initial render
    - Impact: 221ms potential savings on FCP
    - Critical resources from Google Fonts and site CSS

  LCP Breakdown Analysis

  The LCP element is an image (ai-homepage-card_1440.png) with timing breakdown:
  - TTFB: 935ms (60.8%) - Server response delay
  - Resource load delay: 361ms (23.5%) - Delay before loading LCP image
  - Resource load duration: 4ms (0.3%) - Actual image download time
  - Element render delay: 238ms (15.4%) - Browser rendering delay

  Recommendations

  1. Server Optimization: Reduce server response time below 600ms target
  2. CSS Optimization: Inline critical CSS or defer non-essential stylesheets
  3. Resource Prioritization: Preload LCP image earlier in the loading process
  4. Font Loading: Consider using font-display: swap for better loading performance

  The page shows good resource loading efficiency once the server responds, but the initial server latency is the primary bottleneck affecting overall performance.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
&amp;gt;
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ? for shortcuts                                                                                                                                                        0 tokens
                                                                                                                                    globalVersion: 2.0.31 · latestVersion: 2.0.42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些对我们日常使用没有什么意义, 可以直接退出对话&lt;/p&gt;
&lt;h3&gt;下载抖音视频&lt;/h3&gt;
&lt;p&gt;我们直接输入提示词: &lt;code&gt;调用 chrome-devtools-mcp 访问 www.douyin.com, 并搜索 &apos;石榴玫瑰拿铁&apos;, 输出点赞量最高的视频信息并将视频下载到当前目录下&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⏺ 任务完成总结

  我已经成功完成了你的要求：

  ✅ 完成的任务：

  1. 访问抖音网站 - 使用 Chrome DevTools MCP 成功访问 http://www.douyin.com
  2. 搜索关键词 - 搜索了&quot;石榴玫瑰拿铁&quot;
  3. 分析搜索结果 - 找到了多个相关视频并分析了点赞量
  4. 找到最佳视频 - 确定了点赞量最高的相关视频

  🎯 最佳视频信息：

  - 标题： 石榴红茶拿铁｜秋日限定 是谁还没喝过 快做一杯尝尝吧
  - 作者： M+
  - 点赞量： 985个赞
  - 视频ID： 7548036547938454834
  - 发布时间： 2025-09-09 18:36

  📄 输出文件：

  已创建 石榴玫瑰拿铁视频信息.md 文件，包含详细的视频信息和下载方案。

  ⬇️ 关于下载：

  由于抖音的视频链接具有时效性和特殊的认证要求，直接从浏览器下载比较困难。我已经在信息文件中提供了几种下载方案，包括使用第三方下载工具、手机APP保存或录屏等方法。

  视频页面URL：https://www.douyin.com/video/7548036547938454834
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;限于篇幅, 以上只提供了最后一段对话的输出内容, 实际上它调用了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mcp__chrome-devtools__new_page&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mcp__chrome-devtools__wait_for&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mcp__chrome-devtools__click&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mcp__chrome-devtools__navigate_page&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上本地确实保存了两个文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsd -l

.rw-r--r-- xxx staff 1.8 KB Sun Nov 16 11:27:33 2025  石榴玫瑰拿铁视频信息.md
.rw-r--r-- xxx staff 349 B  Sun Nov 16 11:26:50 2025  石榴红茶拿铁_秋日限定_M+.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是显然 &lt;code&gt;349b&lt;/code&gt; 的视频文件明显是错误的, 我们在对话中指出这个视频的问题:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 下载的 mp4 文件无法播放

⏺ 你说得对，刚才下载的文件只有349字节，显然不是真正的视频文件。这是因为抖音的视频链接有防盗链保护和时效性限制。

  让我尝试一些其他方法来下载视频：

⏺ Bash(which yt-dlp)
  ⎿  /opt/homebrew/bin/yt-dlp

⏺ 很好！系统安装了 yt-dlp。让我尝试使用它来下载抖音视频：

⏺ Bash(yt-dlp --list-extractors | grep -i douyin)
  ⎿  Running…

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Bash command

   yt-dlp --list-extractors | grep -i douyin
   检查yt-dlp是否支持抖音

 Do you want to proceed?
 ❯ 1. Yes
   2. Yes, and don&apos;t ask again for yt-dlp commands in /Users/xxx/projects/blog.xiaban.run
   3. No, and tell Claude what to do differently (esc)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后居然检测到我本地已经安装了 &lt;code&gt;yt-dlp&lt;/code&gt;, 按下 &lt;code&gt;Enter&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⏺ Bash(yt-dlp --list-extractors | grep -i douyin)
  ⎿  Douyin

⏺ 太好了！yt-dlp 支持抖音。让我使用它来下载视频：

⏺ Bash(yt-dlp &quot;https://www.douyin.com/video/7548036547938454834&quot; -o &quot;石榴红茶拿铁_秋日限定_M+.mp4&quot;) timeout: 1m 0s
  ⎿  Running…

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Bash command

   yt-dlp &quot;https://www.douyin.com/video/7548036547938454834&quot; -o &quot;石榴红茶拿铁_秋日限定_M+.mp4&quot;
   使用yt-dlp下载抖音视频

 Do you want to proceed?
 ❯ 1. Yes
   2. Yes, and don&apos;t ask again for yt-dlp commands in /Users/xxx/projects/blog.xiaban.run
   3. No, and tell Claude what to do differently (e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里选择继续执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⏺ Bash(yt-dlp &quot;https://www.douyin.com/video/7548036547938454834&quot; -o &quot;石榴红茶拿铁_秋日限定_M+.mp4&quot;) timeout: 1m 0s
  ⎿  Error: Exit code 1
     WARNING: [Douyin] 7548036547938454834: Failed to parse JSON: Expecting value in &apos;&apos;: line 1 column 1 (char 0)
     ERROR: [Douyin] 7548036547938454834: Fresh cookies (not necessarily logged in) are needed

     [Douyin] Extracting URL: https://www.douyin.com/video/7548036547938454834
     [Douyin] 7548036547938454834: Downloading web detail JSON

⏺ 看起来需要登录cookies才能下载。让我尝试另一种方法，使用其他下载工具或者从浏览器中获取更多视频信息：

⏺ chrome-devtools - evaluate_script (MCP)(function: &quot;() =&amp;gt; {\n  // 尝试获取更多视频相关信息，寻找可能的备用链接\n  try {\n    // 检查所有网络请求相关的信息\n    const
                                         performanceEntries = performance.getEntriesByType(&apos;resource&apos;);\n    const videoUrls = [];\n    \n    for (let entry of performanceEntries)
                                          {\n      if (entry.name.includes(&apos;.mp4&apos;) || \n          entry.name.includes(&apos;video&apos;) || \n          entry.name.includes(&apos;douyinvod.com&apos;)
                                         ||\n          entry.name.includes(&apos;bytecdn.com&apos;)) {\n        videoUrls.push({\n          url: entry.name,\n          type:
                                         entry.initiatorType,\n          size: entry.transferSize\n        });\n      }\n    }\n    \n    // 查找页面中所有可能的视频相关数据\n
                                         const scripts = document.querySelectorAll(&apos;script&apos;);\n    let videoData = null;\n    \n    for (let script of scripts) {\n      if
                                         (script.textContent) {\n        const content = script.textContent;\n        \n        // 查找各种可能的视频URL模式\n        const
                                         urlPatterns = [\n          /playAddr[\&quot;&apos;]?\\s*:\\s*[\&quot;&apos;]([^\&quot;&apos;]+\\.(?:mp4|webm|mov)[^\&quot;&apos;]*)[\&quot;&apos;]/,\n
                                         /downloadAddr[\&quot;&apos;]?\\s*:\\s*[\&quot;&apos;]([^\&quot;&apos;]+\\.(?:mp4|webm|mov)[^\&quot;&apos;]*)[\&quot;&apos;]/,\n          /video_url[\&quot;&apos;]?\\s*:\\s*[\&quot;&apos;]([^\&quot;&apos;]+)[\&quot;&apos;]/,\n
                                                /src[\&quot;&apos;]?\\s*:\\s*[\&quot;&apos;]([^\&quot;&apos;]+\\.(?:mp4|webm|mov)[^\&quot;&apos;]*)[\&quot;&apos;]/\n        ];\n        \n        for (let pattern of urlPatterns)
                                         {\n          const match = content.match(pattern);\n          if (match &amp;amp;&amp;amp; match[1]) {\n            if (!videoUrls.find(v =&amp;gt; v.url ===
                                         match[1])) {\n              videoUrls.push({\n                url: match[1],\n                source: &apos;parsed_from_script&apos;,\n
                                            pattern: pattern.toString()\n              });\n            }\n          }\n        }\n      }\n    }\n    \n    return {\n
                                         videoUrls: videoUrls,\n      pageInfo: {\n        title: document.title,\n        url: window.location.href,\n        userAgent:
                                         navigator.userAgent\n      },\n      timestamp: new Date().toISOString()\n    };\n  } catch (error) {\n    return { error: error.message
                                         };\n  }\n}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可以看到使用 &lt;code&gt;yt-dlp&lt;/code&gt; 命令下载视频时，出现了错误，提示可能需要登录 &lt;code&gt;cookies&lt;/code&gt; 才能下载, 但这显然是不需要 &lt;code&gt;cookies&lt;/code&gt; 就能下载的, 因为我们并没有登录, 通过 &lt;a href=&quot;https://github.com/yt-dlp/yt-dlp/issues/9557&quot;&gt;#9557&lt;/a&gt; 可以看到, 这个问题并没有解决, 所以只能另辟蹊径&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;zhzLuke96/douyin-dl-user-js&quot;}&lt;/p&gt;
&lt;p&gt;经过一番搜索, 找到了 &lt;code&gt;douyin-dl-user-js&lt;/code&gt; 油猴脚本, 我们刚好也是浏览器环境, 实测可以成功下载视频, 我们直接扔给 &lt;code&gt;Claude Code&lt;/code&gt;, 让它通过这种方式下载视频:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 使用 curl 下载全部代码并阅读这个油猴脚本 `https://github.com/zhzLuke96/douyin-dl-user-js/raw/refs/heads/main/dy-dl.user.js`, 使用脚本中的下载逻辑进行视频的下载
2. 调用 chrome-devtools-mcp 访问 www.douyin.com, 并搜索 &apos;石榴玫瑰拿铁&apos;, 输出点赞量最高的视频信息并将视频下载到当前目录下

只下载视频, 最后删除所有产生的中间文件
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
这里的脚本文件可能因为网络问题无法下载, 我们可以直接访问 &lt;a href=&quot;https://github.com/zhzLuke96/douyin-dl-user-js/blob/main/dy-dl.user.js&quot;&gt;https://github.com/zhzLuke96/douyin-dl-user-js/blob/main/dy-dl.user.js&lt;/a&gt; 并下载到本地来使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/chrome-devtools-mcp-example-douyin2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终视频下载成功, 试了一下可以正常播放&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/chrome-devtools-mcp-example-douyin3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;思考&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/chrome-devtools-mcp-diff.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;x&lt;/code&gt; 轴为开发阶段完成任务所需的时间, &lt;code&gt;y&lt;/code&gt; 轴为准确率&lt;/p&gt;
&lt;p&gt;现阶段使用大模型在浏览器端进行复杂的操作简直是 &lt;strong&gt;太慢&lt;/strong&gt; 了, 而且操作的 &lt;strong&gt;准确性很低&lt;/strong&gt;, 无法用它来进行自动化任务, 就比如下载视频的场景, &lt;code&gt;yt-dlp&lt;/code&gt; 是非常快速精准的完成任务, 而使用大模型操作的过程充满了不确定性, 所以自动化任务还是得靠脚本来完成&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以 &amp;lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp&quot; target=&quot;_blank&quot;&amp;gt;chrome-devtools-mcp&amp;lt;/a&amp;gt; 还是更适合简单的任务&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ChromeDevTools/chrome-devtools-mcp&quot;&gt;chrome-devtools-mcp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://github.com/yt-dlp/yt-dlp&quot;&gt;yt-dlp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 yt-dlp + ffmpeg + whisper 下载提取并识别语音文本</title><link>http://blog.xiaban.run/posts/2025/openai-whisper/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/openai-whisper/</guid><description>openai/whisper 是最流行的语音识别工具, 它在 Github 上收获了 91.1k star, 我们来安装一下 whisper, 并在使用 yt-dlp + ffmpeg + whisper 下载提取并提取语音文本, 测试一下中文普通话的识别效果</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;首先需要确保已经安装了 &amp;lt;a href=&quot;https://www.python.org/downloads/&quot; target=&quot;_blank&quot;&amp;gt;python&amp;lt;/a&amp;gt; &lt;strong&gt;3.11 版本&lt;/strong&gt;, 如果本地的 &lt;code&gt;python&lt;/code&gt; 版本不在 &lt;code&gt;3.8 ~ 3.11&lt;/code&gt; 范围内, 可以使用 &lt;code&gt;pyenv&lt;/code&gt; 来安装 &lt;code&gt;python&lt;/code&gt;, 详见 &lt;a href=&quot;#pyenv&quot;&gt;pyenv&lt;/a&gt; 章节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U openai-whisper
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;pyenv&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
对于安装 &lt;code&gt;openai-whisper&lt;/code&gt; 来说, &lt;code&gt;pyenv&lt;/code&gt; 是可选的, &lt;strong&gt;只有在本地 &lt;code&gt;python&lt;/code&gt; 版本不是 &lt;code&gt;3.8 ~ 3.11&lt;/code&gt; 才需要安装&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;whisper&lt;/code&gt; 需要的 &lt;code&gt;python&lt;/code&gt; 版本是 &lt;code&gt;3.8 ~ 3.11&lt;/code&gt;, 所以需要安装 &lt;code&gt;pyenv&lt;/code&gt; 来 &lt;strong&gt;管理和切换 &lt;code&gt;python&lt;/code&gt; 版本&lt;/strong&gt;, 以 &lt;code&gt;MacOS&lt;/code&gt; 环境为例, 我们可以直接使用 &lt;code&gt;brew&lt;/code&gt; 安装&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;brew update
brew install pyenv
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置环境变量, 这里我是用的是 &lt;code&gt;fish shell&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;set -Ux PYENV_ROOT $HOME/.pyenv
test -d $PYENV_ROOT/bin; and fish_add_path $PYENV_ROOT/bin

pyenv init - fish | source
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;切换 &lt;code&gt;python&lt;/code&gt; 版本, 这里我选择的是 &lt;code&gt;3.11.x&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 1. 首先查看 3.11.x 的所有版本(可选)
pyenv install -l | grep 3.11

  3.11.0
  3.11-dev
  3.11.1
  3.11.2
  3.11.3
  3.11.4
  3.11.5
  3.11.6
  3.11.7
  3.11.8
  3.11.9
  3.11.10
  3.11.11
  3.11.12
  3.11.13
  3.11.14
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 2. 设置下载镜像(可选, 这是 fish shell 的设置命令, 其他 shell 请自行修改)
set -Ux PYTHON_BUILD_MIRROR_URL https://mirrors.tuna.tsinghua.edu.cn/python

# 3. 安装 3.11.14
pyenv install 3.11.14

# 4. 切换到 3.11.14, 这里我们选择只在当前 shell 中切换
pyenv shell 3.11.14 # ✅ 只在当前 shell 生效, 不会影响全局的 python 环境
# pyenv local 3.11.14 # 位于当前目录下时自动选择此版本
# pyenv global 3.11.14 # 全局切换版本
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
如果切换时提示 &lt;code&gt;pyenv: shell integration not enabled. Run &apos;pyenv init&apos; for instructions.&lt;/code&gt;, 则需要 执行 &lt;code&gt;pyenv init&lt;/code&gt; 命令, 并且按照输出的提示进行操作, 例如在 &lt;code&gt;fish shell&lt;/code&gt; 下是需要执行 &lt;code&gt;pyenv init - fish | source&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 5. 查看 python 版本
python --version
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
其他系统的安装方式参考 &amp;lt;a href=&quot;https://github.com/pyenv/pyenv?tab=readme-ov-file#installation&quot; target=&quot;_blank&quot;&amp;gt;pyenv - Installation&amp;lt;/a&amp;gt;, &lt;code&gt;pyenv&lt;/code&gt; 并不支持 &lt;code&gt;windows&lt;/code&gt;, 可使用 &amp;lt;a href=&quot;https://github.com/pyenv-win/pyenv-win?tab=readme-ov-file#quick-start&quot; target=&quot;_blank&quot;&amp;gt;pyenv-win&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ffmpeg&lt;/h2&gt;
&lt;p&gt;whisper 需要 &lt;code&gt;ffmpeg&lt;/code&gt; 来处理音频文件, 所以需要安装 &lt;code&gt;ffmpeg&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;你可以直接下载: &amp;lt;a href=&quot;https://ffmpeg.org/download.html&quot; target=&quot;_blank&quot;&amp;gt;ffmpeg - Download&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;或者通过命令行工具安装:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# on Ubuntu or Debian
sudo apt update &amp;amp;&amp;amp; sudo apt install ffmpeg

# on Arch Linux
sudo pacman -S ffmpeg

# on MacOS using Homebrew (https://brew.sh/)
brew install ffmpeg

# on Windows using Chocolatey (https://chocolatey.org/)
choco install ffmpeg

# on Windows using Scoop (https://scoop.sh/)
scoop install ffmpeg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;选择模型和语言&lt;/h2&gt;
&lt;p&gt;有 &amp;lt;a href=&quot;https://github.com/openai/whisper?tab=readme-ov-file#available-models-and-languages&quot; target=&quot;_blank&quot;&amp;gt;以下模型&amp;lt;/a&amp;gt; 可选择:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;English-only model&lt;/th&gt;
&lt;th&gt;Multilingual model&lt;/th&gt;
&lt;th&gt;Required VRAM&lt;/th&gt;
&lt;th&gt;Relative speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tiny&lt;/td&gt;
&lt;td&gt;39 M&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tiny.en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1 GB&lt;/td&gt;
&lt;td&gt;~10x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;base&lt;/td&gt;
&lt;td&gt;74 M&lt;/td&gt;
&lt;td&gt;&lt;code&gt;base.en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1 GB&lt;/td&gt;
&lt;td&gt;~7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;small&lt;/td&gt;
&lt;td&gt;244 M&lt;/td&gt;
&lt;td&gt;&lt;code&gt;small.en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~2 GB&lt;/td&gt;
&lt;td&gt;~4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;769 M&lt;/td&gt;
&lt;td&gt;&lt;code&gt;medium.en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;~2x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;large&lt;/td&gt;
&lt;td&gt;1550 M&lt;/td&gt;
&lt;td&gt;&lt;code&gt;N/A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;large&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~10 GB&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;turbo&lt;/td&gt;
&lt;td&gt;809 M&lt;/td&gt;
&lt;td&gt;&lt;code&gt;N/A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~6 GB&lt;/td&gt;
&lt;td&gt;~8x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;由于我们需要识别的是中文普通话, 所以只能选择 &lt;code&gt;Multilingual model&lt;/code&gt;, 然后我们安装本地的内存大小选择适合的模型, 这里我选择 &lt;code&gt;large&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;接下来我们尝试使用 &amp;lt;a href=&quot;https://github.com/yt-dlp/yt-dlp&quot; target=&quot;_blank&quot;&amp;gt;yd-dlp&amp;lt;/a&amp;gt; 从 &lt;code&gt;bilibili&lt;/code&gt; 获取视频, 然后使用 &lt;code&gt;ffmpeg&lt;/code&gt; 提取其中的音频, 再交给 &lt;code&gt;whisper&lt;/code&gt; 进行语音识别, 最终提取视频中的语音文本:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=68314743&amp;amp;bvid=BV1bJ41137yp&amp;amp;cid=118404651&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;yt-dlp&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
💡 实际上可以直接使用 &lt;code&gt;yt-dlp&lt;/code&gt; 提取音频, &lt;strong&gt;这里为了演示增加了 &lt;code&gt;ffmpeg&lt;/code&gt; 提取的操作, 详见 &lt;a href=&quot;#%E7%9B%B4%E6%8E%A5%E4%B8%8B%E8%BD%BD%E9%9F%B3%E9%A2%91&quot;&gt;直接下载音频&lt;/a&gt; 章节&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们随便找一个自己喜欢的视频, 这里为了直观的感受语音识别的准确率, 我们找一个翻唱视频:&lt;/p&gt;
&lt;h4&gt;下载视频&lt;/h4&gt;
&lt;p&gt;由于 B 站是视频和音频分离的, 所以我们可以直接下载音频文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看可以下载的视频资源格式
yt-dlp &quot;https://www.bilibili.com/video/BV1bJ41137yp&quot; -F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们按照 id 来下载视频和音频文件, 最终下载的是 &lt;code&gt;.mp4&lt;/code&gt; 格式的音频文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yt-dlp &quot;https://www.bilibili.com/video/BV1bJ41137yp&quot; -f 30280+100026 -o &quot;说好不哭.mp4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;提取音频&lt;/h4&gt;
&lt;p&gt;我们使用 &lt;code&gt;ffmpeg&lt;/code&gt; 提取音频文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i 说好不哭.mp4 -vn -c:a copy 说好不哭-audio.m4a
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;直接下载音频&lt;/h4&gt;
&lt;p&gt;或者我们选择 &lt;code&gt;audio only&lt;/code&gt; 中质量最高的音频文件进行下载&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yt-dlp &quot;https://www.bilibili.com/video/BV1bJ41137yp&quot; -f 30280 -o &quot;说好不哭.m4a&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;使用 whisper 识别语音&lt;/h3&gt;
&lt;p&gt;最终, 我们得到了此视频的音频文件 &lt;code&gt;说好不哭.m4a&lt;/code&gt;, 我们使用 &lt;code&gt;whisper&lt;/code&gt; 识别:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;whisper 说好不哭.m4a --model large --language Chinese
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
首次执行需要下载对应的模型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里不知道为什么, 识别过程非常慢, 总共花了 &lt;code&gt;8.1m&lt;/code&gt;, 查看 &lt;code&gt;CPU&lt;/code&gt; 及 内存使用情况也没有发现问题:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili6.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/openai-whisper-yt-dlp-bilibili7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是输出的语音文本:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:00.000 --&amp;gt; 00:04.360] 没有了联络
[00:04.360 --&amp;gt; 00:06.120] 后来的生活
[00:06.120 --&amp;gt; 00:09.040] 我都是听别人说
[00:09.040 --&amp;gt; 00:11.440] 说你怎么了
[00:11.440 --&amp;gt; 00:13.500] 说你怎么过
[00:13.500 --&amp;gt; 00:16.320] 放不下的人是我
[00:16.320 --&amp;gt; 00:20.220] 人多的时候就待在角落
[00:20.220 --&amp;gt; 00:23.040] 就把别人问起我
[00:23.040 --&amp;gt; 00:25.340] 你们怎么了
[00:25.340 --&amp;gt; 00:26.700] 你低着头
[00:26.700 --&amp;gt; 00:29.940] 护着我连抱怨都没有
[00:29.940 --&amp;gt; 00:32.240] 电话开始拖
[00:32.240 --&amp;gt; 00:34.040] 总不对我说
[00:34.040 --&amp;gt; 00:36.740] 不习惯一个人生活
[00:36.740 --&amp;gt; 00:39.140] 离开我以后
[00:39.140 --&amp;gt; 00:40.880] 要我还好过
[00:40.880 --&amp;gt; 00:43.840] 怕打扰像自由的我
[00:43.840 --&amp;gt; 00:47.260] 都这个时候你还在意着
[00:47.260 --&amp;gt; 00:50.520] 别人是怎么怎么干我的
[00:50.520 --&amp;gt; 00:53.680] 明明解释这不是我的错
[00:53.680 --&amp;gt; 00:55.940] 是你要走
[00:55.940 --&amp;gt; 01:01.600] 掩盖着你难过
[01:01.600 --&amp;gt; 01:08.000] 挽留的话却没有说
[01:08.000 --&amp;gt; 01:14.980] 你会微笑放手
[01:14.980 --&amp;gt; 01:21.640] 说好无辜让我走
[01:24.880 --&amp;gt; 01:25.040] Zither Harp
[01:25.040 --&amp;gt; 01:25.920] 电话开始拖
[01:25.940 --&amp;gt; 01:26.940] 总不对我说
[01:26.940 --&amp;gt; 01:27.940] 不习惯一个人生活
[01:27.940 --&amp;gt; 01:28.940] 离开我以后
[01:28.940 --&amp;gt; 01:29.940] 要我还好过
[01:29.940 --&amp;gt; 01:30.940] 怕打扰像自由的我
[01:30.940 --&amp;gt; 01:31.940] 都这个时候你还在意着
[01:31.940 --&amp;gt; 01:32.940] 别人是怎么怎么干我的
[01:32.940 --&amp;gt; 01:33.940] 明明解释这不是我的错
[01:33.940 --&amp;gt; 01:34.940] 是你要走
[01:34.940 --&amp;gt; 01:35.940] 掩盖着你难过
[01:35.940 --&amp;gt; 01:36.940] 都这个时候你还在意着
[01:36.940 --&amp;gt; 01:37.940] 别人是怎么怎么干我的
[01:37.940 --&amp;gt; 01:38.940] 明明解释这不是我的错
[01:38.940 --&amp;gt; 01:39.940] 是你要走
[01:39.940 --&amp;gt; 01:40.940] 掩盖着你难过
[01:40.940 --&amp;gt; 01:41.940] 掩盖着你难过
[01:41.940 --&amp;gt; 01:42.940] 掩盖着你难过
[01:42.940 --&amp;gt; 01:43.940] 掩盖着你难过
[01:43.940 --&amp;gt; 01:44.940] 走
[01:44.940 --&amp;gt; 01:45.940] 走
[01:45.940 --&amp;gt; 01:46.940] 走
[01:46.940 --&amp;gt; 01:47.940] 走
[01:47.940 --&amp;gt; 01:48.940] 走
[01:48.940 --&amp;gt; 01:49.940] 走
[01:49.940 --&amp;gt; 01:50.940] 走
[01:50.940 --&amp;gt; 01:51.940] 走
[01:51.940 --&amp;gt; 01:52.940] 走
[01:52.940 --&amp;gt; 01:53.940] 走
[01:53.940 --&amp;gt; 01:54.940] 走
[01:54.940 --&amp;gt; 01:55.940] 走
[01:55.940 --&amp;gt; 01:56.940] 走
[01:56.940 --&amp;gt; 01:57.940] 走
[01:57.940 --&amp;gt; 01:58.940] 走
[01:58.940 --&amp;gt; 01:59.940] 走
[01:59.940 --&amp;gt; 02:00.940] 走
[02:00.940 --&amp;gt; 02:01.940] 走
[02:01.940 --&amp;gt; 02:02.940] 走
[02:02.940 --&amp;gt; 02:03.940] 走
[02:03.940 --&amp;gt; 02:04.940] 走
[02:04.940 --&amp;gt; 02:05.940] 走
[02:05.940 --&amp;gt; 02:06.940] 走
[02:06.940 --&amp;gt; 02:07.940] 走
[02:07.940 --&amp;gt; 02:08.940] 走
[02:08.940 --&amp;gt; 02:09.940] 走
[02:09.940 --&amp;gt; 02:10.940] 走
[02:10.940 --&amp;gt; 02:11.940] 走
[02:11.940 --&amp;gt; 02:12.940] 走
[02:12.940 --&amp;gt; 02:13.940] 走
[02:13.940 --&amp;gt; 02:14.940] 走
[02:14.940 --&amp;gt; 02:15.940] 走
[02:15.940 --&amp;gt; 02:16.940] 走
[02:16.940 --&amp;gt; 02:17.940] 走
[02:17.940 --&amp;gt; 02:18.940] 走
[02:18.940 --&amp;gt; 02:19.940] 走
[02:19.940 --&amp;gt; 02:20.940] 走
[02:20.940 --&amp;gt; 02:21.940] 走
[02:21.940 --&amp;gt; 02:22.940] 走
[02:22.940 --&amp;gt; 02:23.940] 走
[02:23.940 --&amp;gt; 02:24.940] 走
[02:24.940 --&amp;gt; 02:25.940] 走
[02:25.940 --&amp;gt; 02:32.260] 还在为我梦加油
[02:32.260 --&amp;gt; 02:39.340] 心疼过了多久
[02:39.340 --&amp;gt; 02:45.340] 还在找理由等我
[02:55.940 --&amp;gt; 03:25.920] 由 Amara.org 社群提供的字幕
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个执行速度实在太慢, 识别准确率不忍直视(可能是因为是唱歌的原因?), 我们尝试使用小一点的模型来执行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;whisper 说好不哭.m4a --model medium --language Chinese
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终花费 &lt;code&gt;1.4m&lt;/code&gt;, 识别结果如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:00.000 --&amp;gt; 00:06.600] 沒有了電絡 後來的生活
[00:06.600 --&amp;gt; 00:10.100] 我都是聽別人說
[00:10.100 --&amp;gt; 00:13.800] 說你怎麼了 說你怎麼過
[00:13.800 --&amp;gt; 00:17.300] 放不下的人是我
[00:17.300 --&amp;gt; 00:20.700] 人多的時候就待在角落
[00:20.700 --&amp;gt; 00:24.100] 就怕別人問起我
[00:24.100 --&amp;gt; 00:26.700] 你們怎麼了 你低著頭
[00:26.700 --&amp;gt; 00:30.900] 扶著我 連抱怨都沒有
[00:30.900 --&amp;gt; 00:34.300] 電話開始偷 總不對我說
[00:34.300 --&amp;gt; 00:37.700] 不習慣一個人生活
[00:37.700 --&amp;gt; 00:41.100] 離開我以後 要我好過
[00:41.100 --&amp;gt; 00:44.500] 怕它繞向自由的我
[00:44.500 --&amp;gt; 00:47.800] 都這個時候 你還在意著
[00:47.800 --&amp;gt; 00:51.200] 別人是怎麼 怎麼幹我的
[00:51.200 --&amp;gt; 00:53.700] 拼命解釋這不是我的錯
[00:53.700 --&amp;gt; 00:56.300] 是你要走
[00:56.300 --&amp;gt; 01:01.800] 眼看著你 難過
[01:03.800 --&amp;gt; 01:08.300] 溫柔的話卻沒有說
[01:10.300 --&amp;gt; 01:15.300] 你會怪笑 放手
[01:17.300 --&amp;gt; 01:22.300] 說好不顧 讓我走
[01:23.300 --&amp;gt; 01:28.300] 電話開始偷 總不對我說
[01:28.300 --&amp;gt; 01:31.300] 不習慣一個人生活
[01:31.300 --&amp;gt; 01:35.300] 離開我以後 要我好過
[01:35.300 --&amp;gt; 01:38.300] 怕它繞向自由的我
[01:38.300 --&amp;gt; 01:42.300] 都這個時候 你還在意著
[01:42.300 --&amp;gt; 01:45.300] 別人是怎麼 怎麼幹我的
[01:45.300 --&amp;gt; 01:48.300] 拼命解釋這不是我的錯
[01:48.300 --&amp;gt; 01:51.300] 是你要走
[01:51.300 --&amp;gt; 01:56.300] 眼看著你 難過
[01:58.300 --&amp;gt; 02:03.300] 溫柔的話卻沒有說
[02:05.300 --&amp;gt; 02:10.300] 你會怪笑 放手
[02:12.300 --&amp;gt; 02:16.300] 說好不顧 讓我走
[02:18.300 --&amp;gt; 02:22.300] 沒什麼都沒有
[02:25.300 --&amp;gt; 02:30.300] 卻還在為我夢加油
[02:32.300 --&amp;gt; 02:37.300] 想等過了多久
[02:39.300 --&amp;gt; 02:44.300] 還在找理由等我
[02:48.300 --&amp;gt; 02:53.300] 詞曲 李宗盛
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的竟然是繁体字... 看来用来训练的语料基本都来自 &lt;code&gt;TW&lt;/code&gt; 😭, &lt;code&gt;anyway&lt;/code&gt;, 我们在尝试一下使用比特率最低的音频文件来进行识别:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yt-dlp &quot;https://www.bilibili.com/video/BV1bJ41137yp&quot; -f 30216 -o &quot;说好不哭-30216.m4a&quot;

[BiliBili] Extracting URL: https://www.bilibili.com/video/BV1bJ41137yp
[BiliBili] 1bJ41137yp: Downloading webpage
[BiliBili] BV1bJ41137yp: Extracting videos in anthology
[BiliBili] BV1bJ41137yp: Downloading wbi sign
[BiliBili] BV1bJ41137yp: Downloading video formats for cid 118404651
[BiliBili] Format(s) 1080P 高码率 are missing; you have to become a premium member to download them. Use --cookies-from-browser or --cookies for the authentication. See  https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp  for how to manually pass cookies
[BiliBili] 68314743: Extracting chapters
[info] BV1bJ41137yp: Downloading 1 format(s): 30216
[download] Destination: 说好不哭-30216.m4a
[download] 100% of    1.51MiB in 00:00:00 at 4.86MiB/s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;whisper 说好不哭-30216.m4a --model medium --language Chinese
[00:00.000 --&amp;gt; 00:10.000] 沒有了聯絡 後來的生活 我都是聽別人說
[00:10.000 --&amp;gt; 00:17.000] 說你怎麼了 說你怎麼過 放不下的人是我
[00:17.000 --&amp;gt; 00:24.000] 人多的時候就待在角落 就怕別人問起我
[00:24.000 --&amp;gt; 00:31.000] 你們怎麼了 你低著頭 捕捉我 連抱怨都沒有
[00:31.000 --&amp;gt; 00:38.000] 電話開始偷 總不對我說 不習慣一個人生活
[00:38.000 --&amp;gt; 00:44.000] 離開我以後 要我好好過 怕它擾向自由的我
[00:44.000 --&amp;gt; 00:51.000] 都這個時候 你還在意著 別人是怎麼 怎麼幹我的
[00:51.000 --&amp;gt; 00:56.000] 拼命解釋這不是我的錯 是你要走
[00:56.000 --&amp;gt; 01:03.000] 眼看著你難過
[01:03.000 --&amp;gt; 01:10.000] 忘了的話卻沒有說
[01:10.000 --&amp;gt; 01:17.000] 你會怪笑放手
[01:17.000 --&amp;gt; 01:25.000] 說好不顧讓我走
[01:25.000 --&amp;gt; 01:32.000] 電話開始偷 總不對我說 不習慣一個人生活
[01:32.000 --&amp;gt; 01:38.000] 離開我以後 要我好好過 怕它擾向自由的我
[01:38.000 --&amp;gt; 01:45.000] 都這個時候 你還在意著 別人是怎麼 怎麼幹我的
[01:45.000 --&amp;gt; 01:51.000] 拼命解釋這不是我的錯 是你要走
[01:51.000 --&amp;gt; 01:58.000] 眼看著你難過
[01:58.000 --&amp;gt; 02:05.000] 忘了的話卻沒有說
[02:05.000 --&amp;gt; 02:12.000] 你會怪笑放手
[02:12.000 --&amp;gt; 02:18.000] 說好不顧讓我走
[02:18.000 --&amp;gt; 02:25.000] 你什麼都沒有
[02:25.000 --&amp;gt; 02:32.000] 卻還在為我夢加油
[02:32.000 --&amp;gt; 02:39.000] 想藏過了多久
[02:39.000 --&amp;gt; 02:44.000] 還在找理由等我
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终耗时 &lt;code&gt;1.2m&lt;/code&gt; 完成, 看来使用 &lt;code&gt;medium&lt;/code&gt; 模型识别比特率较低的音频文件是最合适的方式, 虽然识别准确率很一般&lt;/p&gt;
&lt;p&gt;最后附上小姐姐的翻唱视频链接: &amp;lt;a href=&quot;https://www.bilibili.com/video/BV1bJ41137yp/?vd_source=19e09fa462750e58610f95b1a63dbfe7&quot; target=&quot;_blank&quot;&amp;gt;🎬 说好不哭 女正版 cover by 刘蕴晴 Rachel&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;参考来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/yt-dlp/yt-dlp&quot; target=&quot;_blank&quot;&amp;gt;yd-dlp&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/pyenv/pyenv?tab=readme-ov-file#installation&quot; target=&quot;_blank&quot;&amp;gt;pyenv - Installation&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;封面图素材来自: &amp;lt;a href=&quot;https://www.flaticon.com/free-icons/whisper&quot; title=&quot;whisper icons&quot;&amp;gt;Whisper icons created by Freepik - Flaticon&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>我在工作日使用的 ios 快捷指令</title><link>http://blog.xiaban.run/posts/2025/ios-shotcut-working-day/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/ios-shotcut-working-day/</guid><description>这是一篇关于我如何用 iOS 快捷指令自动化工作日生活的分享。从自动判断是否为工作日，到自动启动闹钟、钉钉打卡、车载蓝牙触发导航，一切都在后台悄然完成。让繁琐的日常变得智能又轻松。</description><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!TIP]
友情提示: 本文中的 &lt;strong&gt;快捷指令&lt;/strong&gt; 指的是 IOS 的系统应用, 只能在苹果设备上使用, 安卓各大厂的系统也有类似的功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;快捷指令有什么用&lt;/h2&gt;
&lt;p&gt;作为一个牛马打工人, 一定有过 &lt;strong&gt;忘记打卡&lt;/strong&gt; 的经历, 搬了一天砖, 发现居然忘记打卡了 😡, 补卡却要领导层层审批, 还要解释加证明自己真的来板砖了...&lt;/p&gt;
&lt;p&gt;还有就是节假日的 &lt;strong&gt;补班日直接睡过头了&lt;/strong&gt;, 平时订的闹钟都是周一到周五, 到补班那天闹钟没响, 等起床了发现根本来不及了, 我就想问是谁发明了补班这么个玩意儿, 我真想一🧱拍死他&lt;/p&gt;
&lt;p&gt;自动化的快捷指令就可以解决这两个问题, 让我们 &lt;strong&gt;在工作日执行一些自动化的工作流&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;今天是不是工作日&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-holiday.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么如何判断是否为工作日呢? 在 &lt;code&gt;IOS&lt;/code&gt; 的 📅 日历 APP 中, 点击底部的 &lt;strong&gt;日历&lt;/strong&gt; 按钮, 再点击 &lt;strong&gt;添加日历&lt;/strong&gt;, 搜索 &lt;strong&gt;中国(中文)&lt;/strong&gt;, 就可以订阅 &lt;strong&gt;中国大陆节假日&lt;/strong&gt;, 然后在日历中就会 &lt;strong&gt;显示节假日相关的日程&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-calendar.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们就可以实现通过读取当前日程, 判断今天是否是工作日&lt;/p&gt;
&lt;h2&gt;工作日闹钟&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
可以直接 &amp;lt;a href=&quot;https://www.icloud.com/shortcuts/85a171d7627748fbbb6975cf6acdffcd&quot; target=&quot;_blank&quot;&amp;gt;🔗 点击这里&amp;lt;/a&amp;gt; 获取 工作日闹钟 快捷指令, &lt;strong&gt;⚠️ 需要把闹钟替换成自己的 ⏰ 起床闹钟&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建一个快捷指令, 从日历中读取 包含 &lt;code&gt;（班）&lt;/code&gt; &lt;code&gt;（休）&lt;/code&gt; 的日程, 就可以判断补班和节假日, 然后再判断是否是周末, 如果是工作日, 就开启闹钟, 否则关闭闹钟&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-alarm-clock.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意这里选择的 ⏰ 闹钟 是自己的起床时间的闹钟&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-auto.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们点击底部的 &lt;strong&gt;自动化&lt;/strong&gt;, 选择 &lt;strong&gt;特定时间运行&lt;/strong&gt;, 然后再添加创建好的 &lt;strong&gt;工作日闹钟 v1&lt;/strong&gt; 快捷指令, 然后设置为 &lt;strong&gt;立即运行&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快捷指令在创建好之后, &lt;strong&gt;必须手动执行一次&lt;/strong&gt; 才会生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化任务首次执行也需要手动确认&lt;/strong&gt;, 之后才会自动运行&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;钉钉打卡&lt;/h2&gt;
&lt;p&gt;虽然钉钉提供了打卡的快捷指令, 但如果你加入了多个公司, 那么在打卡时就需要选择在那个公司打卡, 这太蠢了, 好在我们可以使用 &lt;code&gt;URL Scheme&lt;/code&gt; 直接调起钉钉的打卡入口, 在 &lt;code&gt;URL&lt;/code&gt; 中可以通过 &lt;code&gt;corpId&lt;/code&gt; 参数指定公司:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-dingtalk-url.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击底部的 &lt;strong&gt;通讯录&lt;/strong&gt; - &lt;strong&gt;邀请&lt;/strong&gt;, 然后截屏, 在点击右下角的扫描按钮, 将 &lt;code&gt;URL&lt;/code&gt; 复制出来, 其中的 &lt;code&gt;ecoCorpId&lt;/code&gt; 就是 &lt;code&gt;URL Scheme&lt;/code&gt; 中需要替换的 &lt;code&gt;corpId&lt;/code&gt; 参数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;邀请链接:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;https://n.dingtalk.com/dingding/ecological_org/invite/index.html?dtaction=os&amp;amp;dd_darkmode=true&amp;amp;unionOrgId=257q04350302&amp;amp;ecoCorpId=dingaoefjoaiOIfoejaiojfOIDSJI&amp;amp;inviterUid=&amp;amp;inviteCode=asoiejfOSJfiea&amp;amp;deptId=-1#/inviteJoin
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ecoCorpId&lt;/code&gt;: &lt;code&gt;dingaoefjoaiOIfoejaiojfOIDSJI&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终的 &lt;code&gt;URL Scheme&lt;/code&gt;: &lt;code&gt;dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html?corpId=dingaoefjoaiOIfoejaiojfOIDSJI&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后我们创建一个快捷指令, 直接打开这个 &lt;code&gt;URL&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-dingtalk-checkin.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我先打开了蜂窝数据, 然后震动三下, 播放了一段声音, 然后判断当前是上班还是下班, 最后打开这个 &lt;code&gt;URL&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-dingtalk-checkin-auto.png&quot; alt=&quot;&quot; /&gt;
最后我们依然是像添加工作日闹钟一样, 创建一个指定时间的自动化任务, 然后添加这个快捷指令&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快捷指令在创建好之后, &lt;strong&gt;必须手动执行一次&lt;/strong&gt; 才会生效&lt;/li&gt;
&lt;li&gt;钉钉打卡 &lt;strong&gt;需要解锁手机才能执行&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;驾车通勤&lt;/h2&gt;
&lt;p&gt;由于每天开车通勤, 所以打卡的最佳时间点应该是 上车 / 下车 时(连接 &lt;code&gt;carplay&lt;/code&gt; 或连接车载蓝牙)打卡, 当然也可以是到达指定位置或者连接指定 &lt;code&gt;WIFI&lt;/code&gt; 时打卡, 这些快捷指令都支持!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-auto-select.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我目前 上车 / 下车 时都会执行快捷指令:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上车: 如果是工作日, 就判断时间
&lt;ul&gt;
&lt;li&gt;上班时间: 打开蜂窝网络, 播放歌曲, 导航到公司&lt;/li&gt;
&lt;li&gt;下班时间: 钉钉打卡, 播放音乐, 导航到家&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;下车: 如果是工作日, 就判断时间
&lt;ul&gt;
&lt;li&gt;上班时间: 钉钉打卡&lt;/li&gt;
&lt;li&gt;下班时间: &lt;em&gt;暂无&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/ios-shotcut-working-day-carplay.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>mcp-chrome - 让 Claude Code 接管你的 Chrome</title><link>http://blog.xiaban.run/posts/2025/claude-code-mcp-chrome/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/claude-code-mcp-chrome/</guid><description>介绍 mcp-chrome 的安装和使用, 以及如何完善我们的工作流</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍 &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; 的安装和使用, 以及如何使用 &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; 完善我们的工作流&lt;/p&gt;
&lt;h2&gt;浏览器自动化&lt;/h2&gt;
&lt;p&gt;目前 &lt;strong&gt;通过程序调用浏览器来完成某些任务&lt;/strong&gt;, 可以实现的库有以下几种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器端到端自动化测试库: 例如 &amp;lt;a href=&quot;https://playwright.dev/&quot; target=&quot;_blank&quot;&amp;gt;playwright&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://www.selenium.dev/&quot; target=&quot;_blank&quot;&amp;gt;selenium&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://www.cypress.io/&quot; target=&quot;_blank&quot;&amp;gt;cypress&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;浏览器自动化库: &amp;lt;a href=&quot;https://pptr.dev/&quot; target=&quot;_blank&quot;&amp;gt;puppeteer&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;基于浏览器插件的自动化库: &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://github.com/Oanakiaja/chrome-extension-bridge-mcp&quot; target=&quot;_blank&quot;&amp;gt;chrome-extension-bridge-mcp&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与同类项目对比:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比维度&lt;/th&gt;
&lt;th&gt;基于浏览器端到端自动化测试库 / 浏览器自动化库&lt;/th&gt;
&lt;th&gt;基于Chrome插件的MCP Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;资源占用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 需启动独立浏览器进程，需要安装Playwright依赖，下载浏览器二进制等&lt;/td&gt;
&lt;td&gt;✅ 无需启动独立的浏览器进程，直接利用用户已打开的Chrome浏览器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;用户会话复用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 需重新登录&lt;/td&gt;
&lt;td&gt;✅ 自动使用已登录状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;浏览器环境保持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 干净环境缺少用户设置&lt;/td&gt;
&lt;td&gt;✅ 完整保留用户环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API访问权限&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ 受限于Playwright API&lt;/td&gt;
&lt;td&gt;✅ Chrome原生API全访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;启动速度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 需启动浏览器进程&lt;/td&gt;
&lt;td&gt;✅ 只需激活插件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;响应速度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50-200ms进程间通信&lt;/td&gt;
&lt;td&gt;✅ 更快&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;简而言之, &lt;code&gt;mcp-chrome&lt;/code&gt; 更适合在本地环境使用, 可以 &lt;strong&gt;让 &lt;code&gt;AI&lt;/code&gt; 直接接管我们的 &lt;code&gt;Chrome&lt;/code&gt;, 而不是没有任何上下文的空白浏览器&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;hangwin/mcp-chrome&quot;}&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; 是一个基于 &lt;code&gt;chrome&lt;/code&gt; 插件的 &lt;strong&gt;模型上下文协议 (MCP) 服务器&lt;/strong&gt;，它将您的 &lt;code&gt;Chrome&lt;/code&gt; 浏览器功能暴露给 &lt;code&gt;Claude&lt;/code&gt; 等 &lt;code&gt;AI&lt;/code&gt; 助手，实现复杂的浏览器自动化、内容分析和语义搜索等。与传统的浏览器自动化工具（如 &lt;code&gt;playwright&lt;/code&gt;）不同，&lt;strong&gt;&amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt;&lt;/strong&gt; 直接使用您日常使用的 &lt;code&gt;chrome&lt;/code&gt; 浏览器，基于现有的用户习惯和配置、登录态，让各种大模型或者各种 &lt;code&gt;chatbot&lt;/code&gt; 都可以接管你的浏览器，真正成为你的如常助手&lt;/p&gt;
&lt;h3&gt;✨ 核心特性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;😁 &lt;strong&gt;chatbot/模型无关&lt;/strong&gt;：让任意你喜欢的llm或chatbot客户端或agent来自动化操作你的浏览器&lt;/li&gt;
&lt;li&gt;⭐️ &lt;strong&gt;使用你原本的浏览器&lt;/strong&gt;：无缝集成用户本身的浏览器环境（你的配置、登录态等）&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;完全本地运行&lt;/strong&gt;：纯本地运行的mcp server，保证用户隐私&lt;/li&gt;
&lt;li&gt;🚄 &lt;strong&gt;Streamable http&lt;/strong&gt;：Streamable http的连接方式&lt;/li&gt;
&lt;li&gt;🏎 &lt;strong&gt;跨标签页&lt;/strong&gt; 跨标签页的上下文&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;语义搜索&lt;/strong&gt;：内置向量数据库和本地小模型，智能发现浏览器标签页内容&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;智能内容分析&lt;/strong&gt;：AI 驱动的文本提取和相似度匹配&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;20+ 工具&lt;/strong&gt;：支持截图、网络监控、交互操作、书签管理、浏览历史等20多种工具&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;SIMD 加速 AI&lt;/strong&gt;：自定义 WebAssembly SIMD 优化，向量运算速度提升 4-8 倍&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;环境要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Node.js&lt;/code&gt; &amp;gt;= 18.19.0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chrome/Chromium&lt;/code&gt; 浏览器&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;安装浏览器插件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问 &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome/releases&quot; target=&quot;_blank&quot;&amp;gt;GitHub Release&amp;lt;/a&amp;gt; 下载 &lt;code&gt;chrome-mcp-server-*.*.*.zip&lt;/code&gt;, 并解压&lt;/li&gt;
&lt;li&gt;访问 &lt;code&gt;chrome://extensions/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;开启 &lt;strong&gt;开发者模式&lt;/strong&gt;
&lt;img src=&quot;./assets/images/mcp-chrome-extensions-page1.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;加载未打包的扩展程序&lt;/strong&gt;
&lt;img src=&quot;./assets/images/mcp-chrome-extensions-page2.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;选择解压后的目录, 例如 &lt;code&gt;chrome-mcp-server-*.*.*&lt;/code&gt;
&lt;img src=&quot;./assets/images/mcp-chrome-extensions-page3.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;安装 mcp-chrome&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 npm 安装:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;npm i -g mcp-chrome-bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;或使用 pnpm:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 方法1：全局启用脚本（推荐）
pnpm config set enable-pre-post-scripts true
pnpm install -g mcp-chrome-bridge

# 方法2：如果 postinstall 没有运行，手动注册
pnpm install -g mcp-chrome-bridge
mcp-chrome-bridge register
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MCP&lt;/h2&gt;
&lt;p&gt;目前 &lt;code&gt;Claude Code&lt;/code&gt; 支持两种服务连接方式:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接入方式&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;性能&lt;/th&gt;
&lt;th&gt;延迟&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http(s)&lt;/code&gt; / &lt;code&gt;websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基于 &lt;code&gt;http(s)&lt;/code&gt; / &lt;code&gt;ws&lt;/code&gt; 的连接方式&lt;/td&gt;
&lt;td&gt;稍差&lt;/td&gt;
&lt;td&gt;稍差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stdio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;基于 &lt;code&gt;stdio&lt;/code&gt; 的连接方式, 只能本地调用&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;快速&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里 &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; 就非常适合使用 &lt;code&gt;stdio&lt;/code&gt; 的方式进行连接, 因为 &amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt; 调用的是本地的 &lt;code&gt;Chrome&lt;/code&gt;, 没有必要启动 &lt;code&gt;http(s)&lt;/code&gt; 服务&lt;/p&gt;
&lt;p&gt;了解更多 &lt;code&gt;MCP&lt;/code&gt; 安装和配置方式, 可参考 &amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com&quot; target=&quot;_blank&quot;&amp;gt;通过 MCP 将 Claude Code 连接到工具 - 官方文档&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;在 Claude Code 中使用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;查看 全局安装的 &lt;code&gt;mcp-chrome-bridge&lt;/code&gt; 路径&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pnpm list -g mcp-chrome-bridge

Legend: production dependency, optional only, dev only

/Users/xxx/Library/pnpm/global/5

dependencies:
mcp-chrome-bridge 1.0.29
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设上面的命令输出的路径是: &lt;code&gt;/Users/xxx/Library/pnpm/global/5&lt;/code&gt; 那么你的最终路径就是: &lt;code&gt;/Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;添加 &lt;code&gt;Claude Code&lt;/code&gt; &lt;code&gt;MCP&lt;/code&gt;, 可以根据需要将 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务添加到 &amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com#%E6%9C%AC%E5%9C%B0%E8%8C%83%E5%9B%B4&quot; target=&quot;_blank&quot;&amp;gt;本地&amp;lt;/a&amp;gt; 或者 &amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com#%E9%A1%B9%E7%9B%AE%E8%8C%83%E5%9B%B4&quot; target=&quot;_blank&quot;&amp;gt;项目&amp;lt;/a&amp;gt; / &amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com#%E7%94%A8%E6%88%B7%E8%8C%83%E5%9B%B4&quot; target=&quot;_blank&quot;&amp;gt;用户&amp;lt;/a&amp;gt; 范围&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;本地范围(只允许当前用户在当前项目中使用): &lt;code&gt;claude mcp add chrome-mcp-stdio -- node /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;项目范围(只在为当前项目添加 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务): &lt;code&gt;claude mcp add chrome-mcp-stdio --scope project -- node /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用户范围(当前用户可在全局使用): &lt;code&gt;claude mcp add chrome-mcp-stdio --scope user -- node /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;应用范围&lt;/th&gt;
&lt;th&gt;是否共享&lt;/th&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com#%E6%9C%AC%E5%9C%B0%E8%8C%83%E5%9B%B4&quot; target=&quot;_blank&quot;&amp;gt;本地范围&amp;lt;/a&amp;gt;&lt;/td&gt;
&lt;td&gt;只允许当前用户在当前项目中使用&lt;/td&gt;
&lt;td&gt;当前项目&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;``&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com#%E9%A1%B9%E7%9B%AE%E8%8C%83%E5%9B%B4&quot; target=&quot;_blank&quot;&amp;gt;项目范围&amp;lt;/a&amp;gt;&lt;/td&gt;
&lt;td&gt;只为当前项目添加 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务, 会写入 &lt;code&gt;.claude.json&lt;/code&gt; 中的 &lt;code&gt;projects.xxx.mcpServers&lt;/code&gt; 中&lt;/td&gt;
&lt;td&gt;当前项目&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--scope project&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com#%E7%94%A8%E6%88%B7%E8%8C%83%E5%9B%B4&quot; target=&quot;_blank&quot;&amp;gt;用户范围&amp;lt;/a&amp;gt;&lt;/td&gt;
&lt;td&gt;为所有项目添加 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务, 会写入 &lt;code&gt;.claude.json&lt;/code&gt; 中的 &lt;code&gt;projects.xxx.mcpServers&lt;/code&gt; 中&lt;/td&gt;
&lt;td&gt;所有项目&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--scope user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里我们选择在 &lt;strong&gt;用户范围&lt;/strong&gt; 添加 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add chrome-mcp-stdio -- node /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js

Added stdio MCP server chrome-mcp-stdio with command: node /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js to user config
File modified: /Users/xxx/.claude.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过输出的内容, 我们发现 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务已经添加到 &lt;code&gt;.claude.json&lt;/code&gt; 文件中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;chrome-mcp-stdio&quot;: {
      &quot;type&quot;: &quot;stdio&quot;,
      &quot;command&quot;: &quot;node&quot;,
      &quot;args&quot;: [
        &quot;/Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js&quot;
      ],
      &quot;env&quot;: {}
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 我们检查一下 &lt;code&gt;Claude Code&lt;/code&gt; 的 &lt;code&gt;MCP&lt;/code&gt; 列表中是否存在 &lt;code&gt;mcp-chrome&lt;/code&gt; 服务:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude mcp list                      (base) 607ms  三  9/17 15:02:20 2025

Checking MCP server health...

chrome-mcp-stdio: node /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js - ✓ Connected
context7: https://mcp.context7.com/mcp (HTTP) - ✓ Connected
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;移除 MCP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;claude mcp remove chrome-mcp-stdio
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;h3&gt;连接服务&lt;/h3&gt;
&lt;p&gt;点击浏览器插件中的链接按钮, 如果连接成功, 则会看到如下界面:
&lt;img src=&quot;./assets/images/mcp-chrome-extension-panel.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果遇到服务未启动, 可能是 &lt;code&gt;mcp-chrome-bridge&lt;/code&gt; 没有注册成功, 可以手动注册:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mcp-chrome-bridge register

Writing Node.js path: /usr/local/bin/node
✓ Node.js path written for run_host scripts
Registering user-level Native Messaging host...
Attempting to register user-level Native Messaging host...
✓ Set execution permissions for index.js
✓ Set execution permissions for run_host.sh
✓ Set execution permissions for cli.js
manifest path==&amp;gt; {
  name: &apos;com.chromemcp.nativehost&apos;,
  description: &apos;Node.js Host for Browser Bridge Extension&apos;,
  path: &apos;/Users/xxx/Library/pnpm/global/5/.pnpm/mcp-chrome-bridge@1.0.29/node_modules/mcp-chrome-bridge/dist/run_host.sh&apos;,
  type: &apos;stdio&apos;,
  allowed_origins: [ &apos;chrome-extension://hbdgbgagpkpjffpklnamcljpakneikee/&apos; ]
} /Users/xxx/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.chromemcp.nativehost.json
Successfully registered user-level Native Messaging host!
Native Messaging host registered successfully!
You can now use connectNative in Chrome extension to connect to this service.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调用 mcp-chrome&lt;/h3&gt;
&lt;p&gt;启动 &lt;code&gt;Claude Code&lt;/code&gt;, 并输入以下内容来调取浏览器获取仓库的 star 数量信息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude

&amp;gt; 调用 Chrome MCP, 检查 Github 上我的所有仓库, 并按 star 数量进行从大到小排序

⏺ I&apos;ll help you check all your GitHub repositories and sort them by star count. Let me use the Chrome MCP to navigate to
  GitHub and retrieve this information.

⏺ chrome-mcp-stdio - chrome_navigate (MCP)(url: &quot;https://github.com/SublimeCT&quot;)

  Todos
  ☐ Navigate to GitHub profile page
  ☐ Extract repository information including star counts
  ☐ Sort repositories by star count in descending order

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Tool use                                                                                                                     │
│                                                                                                                              │
│   chrome-mcp-stdio - chrome_navigate(url: &quot;https://github.com/SublimeCT&quot;) (MCP)                                              │
│   Navigate to a URL or refresh the current tab                                                                               │
│                                                                                                                              │
│ Do you want to proceed?                                                                                                      │
│ ❯ 1. Yes                                                                                                                     │
│   2. Yes, and don&apos;t ask again for chrome-mcp-stdio - chrome_navigate commands in /Users/xxx/projects/blog.xiaban.run       │
│   3. No, and tell Claude what to do differently (esc)                                                                        │
│                                                                                                                              │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用体验&lt;/h2&gt;
&lt;p&gt;实际体验比较差, 没有达到我的预期, 因为 &lt;code&gt;Claude Code&lt;/code&gt; 将操作分解为了很多步骤, 例如&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先获取元素&lt;/li&gt;
&lt;li&gt;再点击 &lt;code&gt;xxx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;再获取数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;...&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实际上 **每一步操作都有时间间隔, 导致操作速度非常慢 **😡, 所以需要多个操作步骤的需求还是应该使用定制化的 &lt;code&gt;MCP&lt;/code&gt; 服务&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome/blob/master/README_zh.md&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome 中文文档&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/hangwin/mcp-chrome/blob/master/docs/TOOLS_zh.md&quot; target=&quot;_blank&quot;&amp;gt;mcp-chrome MCP Server API 中文文档&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://playwright.dev/&quot; target=&quot;_blank&quot;&amp;gt;playwright&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.selenium.dev/&quot; target=&quot;_blank&quot;&amp;gt;selenium&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.cypress.io/&quot; target=&quot;_blank&quot;&amp;gt;cypress&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://pptr.dev/&quot; target=&quot;_blank&quot;&amp;gt;puppeteer&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/Oanakiaja/chrome-extension-bridge-mcp&quot; target=&quot;_blank&quot;&amp;gt;chrome-extension-bridge-mcp&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://docs.claude.com/zh-CN/docs/claude-code/mcp?utm_source=chatgpt.com&quot; target=&quot;_blank&quot;&amp;gt;通过 MCP 将 Claude Code 连接到工具 - 官方文档&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Claude Code 安装与配置指南(接入免费的国产模型)</title><link>http://blog.xiaban.run/posts/2025/claude-code-free/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/claude-code-free/</guid><description>本文将介绍最近爆火的 Claude Code 在国内环境的使用, 包括安装与配置, 以及如何接入免费的国产模型</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文将介绍最近爆火的 &lt;code&gt;Claude Code&lt;/code&gt; &lt;strong&gt;在国内环境的使用&lt;/strong&gt;, 包括安装与配置, 以及如何 &lt;strong&gt;接入国产模型 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;智谱 GLM&amp;lt;/a&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;2025-09-05&lt;/code&gt;, &lt;code&gt;Anthropic&lt;/code&gt; 发布了一篇公告: &lt;a href=&quot;https://www.anthropic.com/news/updating-restrictions-of-sales-to-unsupported-regions&quot;&gt;更新对不受支持地区的销售限制&lt;/a&gt;, 将中国定义为敌对国家, 并且不再为中国公司(包含超过 50% 所有权的中国公司的子公司)提供服务&lt;/p&gt;
&lt;p&gt;这意味着 &lt;strong&gt;在未来我们使用 &lt;code&gt;Claude&lt;/code&gt; 服务的难度将越来越大&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果想要获得最好的 &lt;code&gt;Vibe Coding&lt;/code&gt; 体验, 推荐购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;🔗 GLM Coding Lite&amp;lt;/a&amp;gt; 服务(也可以 &amp;lt;a href=&quot;https://www.bigmodel.cn/activity/trial-card/A8AMOHCHA5&quot;&amp;gt;🔗 点击这里&amp;lt;/a&amp;gt; 先免费试用 7 天), 包月只要 20 💰, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, 每 &lt;code&gt;5&lt;/code&gt; 小时最多约 &lt;code&gt;120&lt;/code&gt; 次 &lt;code&gt;prompts&lt;/code&gt;, 原因如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Claude Code&lt;/code&gt; 消耗的 &lt;code&gt;token&lt;/code&gt; 非常多(我现在一个月已经消耗了 &lt;code&gt;3&lt;/code&gt; 亿多 &lt;code&gt;tokens&lt;/code&gt; 💪), 如果按 &lt;code&gt;token&lt;/code&gt; 消耗量计费会非常贵, 使用包月套餐可以无需担心消耗的 &lt;code&gt;token&lt;/code&gt; 数量, 专注于 &lt;code&gt;Vibe Coding&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Claude Code&lt;/code&gt; 是 &lt;code&gt;Anthropic&lt;/code&gt; 推出的工具, 自然与 &lt;code&gt;Claude&lt;/code&gt; 系列模型支持度最好, 其他大模型与 &lt;code&gt;Claude Code API&lt;/code&gt; 不兼容, 也没有对 &lt;code&gt;Claude Code&lt;/code&gt; 进行优化, 这也是 &lt;code&gt;@musistudio/claude-code-router&lt;/code&gt; 库存在的意义; &lt;strong&gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt; 专门提供了 &lt;code&gt;Claude Code&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;vibe coding&lt;/h2&gt;
&lt;p&gt;最近有个一很火的概念: &lt;code&gt;Vibe Coding&lt;/code&gt;, 直译过来就是 &lt;strong&gt;氛围编程&lt;/strong&gt;, &lt;code&gt;Vibe Coding&lt;/code&gt; 的概念出自 &lt;code&gt;Andrej Karpathy&lt;/code&gt;(前特斯拉人工智能总监, &lt;code&gt;OpenAI&lt;/code&gt; 的创始成员之一) 的一条推文:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vibe-coding.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There&apos;s a new kind of coding I call &quot;vibe coding&quot;, where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. It&apos;s possible because the LLMs (e.g. Cursor Composer w Sonnet) are getting too good. Also I just talk to Composer with SuperWhisper so I barely even touch the keyboard. I ask for the dumbest things like &quot;decrease the padding on the sidebar by half&quot; because I&apos;m too lazy to find it. I &quot;Accept All&quot; always, I don&apos;t read the diffs anymore. When I get error messages I just copy paste them in with no comment, usually that fixes it. The code grows beyond my usual comprehension, I&apos;d have to really read through it for a while. Sometimes the LLMs can&apos;t fix a bug so I just work around it or ask for random changes until it goes away. It&apos;s not too bad for throwaway weekend projects, but still quite amusing. I&apos;m building a project or webapp, but it&apos;s not really coding - I just see stuff, say stuff, run stuff, and copy paste stuff, and it mostly works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;有一种新的编程方式，我称之为“氛围编程”，在这种编程中，你完全沉浸在大脑波中，拥抱指数增长，甚至忘记代码的存在。这是可能的，因为大型语言模型（例如Cursor Composer w Sonnet）变得越来越出色。此外，我只需用SuperWhisper与Composer交谈，几乎都不用碰键盘。我会要求一些愚蠢的事情，比如“将侧边栏的填充减少一半”，因为我懒得去找。我总是“全部接受”，不再阅读差异了。当收到错误信息时，我只是复制粘贴，通常这样就能解决问题。代码超出了我的理解范围，我不得不花很长时间阅读它。有时大型语言模型无法修复错误，我就绕过去或者要求随机更改，直到它消失。对于一次性周末项目来说，这还不错，但仍然很有趣。我正在构建一个项目或网络应用程序，但这并不是真正的编程——我只是看到东西，说出东西，运行东西，复制粘贴东西，而且大部分都能工作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Vibe Coding&lt;/code&gt; 描绘了一个全新的编程方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更少的关注代码, 更多的描述需求&lt;/strong&gt;, 甚至完全通过对话完成编码任务&lt;/li&gt;
&lt;li&gt;只关注结果, &lt;strong&gt;代码质量不再重要&lt;/strong&gt;, 不再做 &lt;code&gt;code review&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要实现 &lt;code&gt;Vibe Coding&lt;/code&gt;, &lt;strong&gt;就需要让 &lt;code&gt;LLM&lt;/code&gt; 了解整个项目, 所以它通常需要更长的上下文和更合适的提示词&lt;/strong&gt;, 更多细节可以参考 &amp;lt;a href=&quot;https://guangzhengli.com/blog/zh/vibe-coding-and-context-coding&quot; target=&quot;_blank&quot;&amp;gt;这篇文章&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;Claude Code&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 是 &lt;code&gt;Anthropic&lt;/code&gt; 公司推出的一个 &lt;code&gt;AI&lt;/code&gt; 编程工具, 它基于 &lt;code&gt;Anthropic&lt;/code&gt; 公司的 &lt;code&gt;Claude&lt;/code&gt; 模型, 相比于 &lt;code&gt;Cursor&lt;/code&gt;, 它只能在命令行中使用, 并且它使用 &lt;code&gt;grep find git cat&lt;/code&gt; 等命令来检索上下文, 更多差异可参考 &amp;lt;a href=&quot;https://guangzhengli.com/blog/zh/vibe-coding-and-context-coding#claude-code&quot; target=&quot;_blank&quot;&amp;gt;这篇文章&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 可以通过 &lt;code&gt;npm / pnpm&lt;/code&gt; 来安装, 所以需要 &lt;code&gt;Nodejs&lt;/code&gt; 环境, 需要先安装 &lt;a href=&quot;http://nodejs.org/zh-cn/download&quot;&gt;Nodejs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# 这里使用的是 npm, 也可以使用 pnpm 或 yarn
npm i -g @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要接入阿里云模型, 则需要安装 &lt;code&gt;@musistudio/claude-code-router&lt;/code&gt;, &lt;strong&gt;使用智谱的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding&amp;lt;/a&amp;gt; 服务, 则无需安装&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i -g @musistudio/claude-code-router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里全局安装了两个包:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt;: 这是 &lt;code&gt;Claude Code&lt;/code&gt; 的核心包, 它提供了 &lt;code&gt;Claude Code&lt;/code&gt; 的功能&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/musistudio/claude-code-router/blob/main/README_zh.md&quot;&amp;gt;@musistudio/claude-code-router&amp;lt;/a&amp;gt;: &lt;strong&gt;是一款可将 &lt;code&gt;Claude Code&lt;/code&gt; 请求路由到不同的模型的工具, 也是实现接入国产模型的关键&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;模型路由: 根据需求将请求路由到不同的模型（例如，后台任务、思考、长上下文）&lt;/li&gt;
&lt;li&gt;多提供商支持: 支持 &lt;code&gt;OpenRouter&lt;/code&gt; / &lt;code&gt;DeepSeek&lt;/code&gt; / &lt;code&gt;Ollama&lt;/code&gt; / &lt;code&gt;Gemini&lt;/code&gt; / &lt;code&gt;Volcengine&lt;/code&gt; / &lt;code&gt;SiliconFlow&lt;/code&gt; 等各种模型提供商&lt;/li&gt;
&lt;li&gt;请求/响应转换: 使用转换器为不同的提供商自定义请求和响应。&lt;/li&gt;
&lt;li&gt;动态模型切换: 在 &lt;code&gt;Claude Code&lt;/code&gt; 中使用 &lt;code&gt;/model&lt;/code&gt; 命令动态切换模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GitHub Actions&lt;/code&gt; 集成: 在您的 &lt;code&gt;GitHub&lt;/code&gt; 工作流程中触发 &lt;code&gt;Claude Code&lt;/code&gt; 任务。&lt;/li&gt;
&lt;li&gt;插件系统: 使用自定义转换器扩展功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;更新 Claude Code&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://www.npmjs.com/package/@anthropic-ai/claude-code&quot; target=&quot;_blank&quot;&amp;gt;&lt;code&gt;Claude Code&lt;/code&gt;&amp;lt;/a&amp;gt; 更新频率非常高, 要获得最佳的使用体验, 应该经常更新 &lt;code&gt;Claude Code&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 这里使用的是 npm, 也可以使用 pnpm 或 yarn
npm i -g @anthropic-ai/claude-code@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;创建 &lt;code&gt;~/.claude-code-router/config.json&lt;/code&gt; 文件(&lt;code&gt;windows&lt;/code&gt; 下为对应的用户目录)&lt;/p&gt;
&lt;p&gt;这里展示了接入 阿里云 / &lt;code&gt;GLM&lt;/code&gt; 模型的方式, 利用各大供应商的免费 &lt;code&gt;tokens&lt;/code&gt; 额度实现免费使用, &lt;strong&gt;如果免费额度用完, 或者想要满足日常使用的需求, 建议购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;大模型提供商&lt;/th&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;计费方式&lt;/th&gt;
&lt;th&gt;是否针对 &lt;code&gt;Anthropic API&lt;/code&gt; 进行兼容优化&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;阿里云&lt;/td&gt;
&lt;td&gt;&amp;lt;a href=&quot;https://bailian.console.aliyun.com/?tab=model#/model-market&quot;&amp;gt;模型广场 - 阿里云百炼&amp;lt;/a&amp;gt;&lt;/td&gt;
&lt;td&gt;有免费额度, &lt;strong&gt;只能按 &lt;code&gt;token&lt;/code&gt; 消耗量付费&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;❌ 不推荐使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;智谱&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GLM 4.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可以购买 &lt;strong&gt;包月/包季/包年 套餐&lt;/strong&gt; &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt;&lt;/td&gt;
&lt;td&gt;提供了兼容 &lt;code&gt;Anthropic API&lt;/code&gt; 的端点, 详见 &amp;lt;a href=&quot;https://docs.bigmodel.cn/cn/guide/develop/claude#claude-code&quot;&amp;gt;接入 &lt;code&gt;Claude Code&lt;/code&gt;&amp;lt;/a&amp;gt;&lt;/td&gt;
&lt;td&gt;✅ 推荐使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
推荐购买 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt; 服务, 包月只要 20 💰, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, 每 &lt;code&gt;5&lt;/code&gt; 小时最多约 &lt;code&gt;120&lt;/code&gt; 次 &lt;code&gt;prompts&lt;/code&gt;, 原因如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Claude Code&lt;/code&gt; 消耗的 &lt;code&gt;token&lt;/code&gt; 非常多(我现在已经消耗了 &lt;code&gt;3&lt;/code&gt; 亿 &lt;code&gt;tokens&lt;/code&gt; 💪), 如果按 &lt;code&gt;token&lt;/code&gt; 消耗量计费会非常贵, 使用包月套餐可以无需担心消耗的 &lt;code&gt;token&lt;/code&gt; 数量, 专注于 &lt;code&gt;Vibe Coding&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Claude Code&lt;/code&gt; 是 &lt;code&gt;Anthropic&lt;/code&gt; 推出的工具, 自然与 &lt;code&gt;Claude&lt;/code&gt; 系列模型支持度最好, 其他大模型与 &lt;code&gt;Claude Code API&lt;/code&gt; 不兼容, 也没有对 &lt;code&gt;Claude Code&lt;/code&gt; 进行优化, 这也是 &lt;code&gt;@musistudio/claude-code-router&lt;/code&gt; 库存在的意义; &lt;strong&gt;&amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt; 专门提供了 &lt;code&gt;Claude Code&lt;/code&gt; 的 &lt;code&gt;API&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;阿里云百炼&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;阿里云百炼有非常多的免费模型, 免费模型普遍都有一百万 &lt;code&gt;token&lt;/code&gt; 额度&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://bailian.console.aliyun.com/?tab=model#/model-market&quot;&gt;模型广场 - 阿里云百炼&lt;/a&gt;, 挑选几个免费模型(例如 &lt;code&gt;通义千问3-Max-Preview&lt;/code&gt;), 点击 &lt;strong&gt;查看详情&lt;/strong&gt;
&lt;img src=&quot;./assets/images/claude-code-aliyun-model-market1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制有免费额度模型的 &lt;strong&gt;&lt;code&gt;Code&lt;/code&gt;&lt;/strong&gt; 值, 并 &lt;strong&gt;启用 免费额度用完即停&lt;/strong&gt;
&lt;img src=&quot;./assets/images/claude-code-aliyun-model-market2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 &lt;code&gt;~/.claude-code-router/config.json&lt;/code&gt; 文件, &lt;strong&gt;将模型添加到 &lt;code&gt;Providers&lt;/code&gt; 数组中&lt;/strong&gt;, 将第二步复制的 &lt;code&gt;Code&lt;/code&gt; 填入 &lt;code&gt;Providers.models&lt;/code&gt;, &lt;strong&gt;并在 &lt;code&gt;Router&lt;/code&gt; 中指定模型路由&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;LOG&quot;: true,
  &quot;HOST&quot;: &quot;127.0.0.1&quot;,
  &quot;APIKEY&quot;: &quot;yourpassword&quot;,
  &quot;PORT&quot;: 65430,
  &quot;Providers&quot;: [
    {
      &quot;name&quot;: &quot;aliyun&quot;,
      &quot;api_base_url&quot;: &quot;https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions&quot;,
      &quot;api_key&quot;: &quot;sk-kljsaoeifklahshgoasieofjio&quot;,
      &quot;models&quot;: [
        &quot;qwen3-max-preview&quot;,
        &quot;qwen-flash&quot;,
        &quot;Moonshot-Kimi-K2-Instruct&quot;,
        &quot;deepseek-v3.1&quot;,
        &quot;deepseek-r1-0528&quot;
      ]
    }
  ],
  &quot;Router&quot;: {
    &quot;default&quot;: &quot;aliyun,qwen3-max-preview&quot;,
    &quot;background&quot;: &quot;GLM,deepseek-v3.1&quot;,
    &quot;think&quot;: &quot;aliyun,deepseek-r1-0528&quot;,
    &quot;longContext&quot;: &quot;GLM,deepseek-v3.1&quot;,
    &quot;longContextThreshold&quot;: 50000,
    &quot;webSearch&quot;: &quot;GLM,qwen3-max-preview&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;请将 &lt;code&gt;api_key&lt;/code&gt; 替换为自己的 &lt;code&gt;api key&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOG&lt;/code&gt;: 打印的日志类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HOST&lt;/code&gt;: &lt;code&gt;claude-code-router&lt;/code&gt; 启动的本地服务; &amp;lt;a href=&quot;https://github.com/musistudio/claude-code-router/blob/main/README_zh.md&quot;&amp;gt;@musistudio/claude-code-router&amp;lt;/a&amp;gt; 的原理就是在本地启动一个服务, 然后让 &lt;code&gt;Claude Code&lt;/code&gt; 去访问本地的服务, 从而实现不访问 &lt;code&gt;Claude API&lt;/code&gt;, 而使用我们指定的国产模型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APIKEY&lt;/code&gt;: &lt;code&gt;Claude Code&lt;/code&gt; 链接服务时使用的 &lt;code&gt;API Key&lt;/code&gt;, 这里我们可以随意填写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PORT&lt;/code&gt;: &lt;code&gt;claude-code-router&lt;/code&gt; 启动的端口, 默认是 &lt;code&gt;6543&lt;/code&gt;, 我习惯修改端口&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/musistudio/claude-code-router/blob/main/README_zh.md#providers&quot; target=&quot;_blank&quot;&amp;gt;Providers&amp;lt;/a&amp;gt;: 模型的提供商, 可以添加多个, 这里我们使用阿里云百炼&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/musistudio/claude-code-router/blob/main/README_zh.md#router&quot; target=&quot;_blank&quot;&amp;gt;Router&amp;lt;/a&amp;gt;: 用于设置路由规则:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;default&lt;/code&gt;: 用于常规任务的默认模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;background&lt;/code&gt;: 用于后台任务的模型。这可以是一个较小的本地模型以节省成本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;think&lt;/code&gt;: 用于推理密集型任务（如计划模式）的模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;longContext&lt;/code&gt;: 用于处理长上下文（例如，&amp;gt; 60K 令牌）的模型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;longContextThreshold&lt;/code&gt; (可选): 触发长上下文模型的令牌数阈值。如果未指定，默认为 60000。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webSearch&lt;/code&gt;: 用于处理网络搜索任务，需要模型本身支持。如果使用openrouter需要在模型后面加上:online后缀。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;(测试版): 用于处理图片类任务（采用CCR内置的agent支持），如果该模型不支持工具调用，需要将config.forceUseImageAgent属性设置为true。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更多参数及配置请参考 &amp;lt;a href=&quot;https://github.com/musistudio/claude-code-router/blob/main/README_zh.md#2-%E9%85%8D%E7%BD%AE&quot; target=&quot;_blank&quot;&amp;gt;官方文档&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;智谱&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;智谱的新用户有 &lt;strong&gt;两千万的免费 &lt;code&gt;tokens&lt;/code&gt; 额度(不同模型)&lt;/strong&gt;, 或者可以使用按 &lt;code&gt;Prompt&lt;/code&gt; 计次的包月服务 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://bigmodel.cn/finance-center/resource-package/package-mgmt&quot;&gt;智谱 BigModel&lt;/a&gt;, 点击右上角的 &lt;strong&gt;财务&lt;/strong&gt;, 点击 &lt;strong&gt;资源包管理 &amp;gt; 我的资源包&lt;/strong&gt; 查看免费额度
&lt;img src=&quot;./assets/images/claude-code-free-glm.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/claude-code-free-glm2.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
由于 &amp;lt;span style=&quot;color: red;&quot;&amp;gt;&lt;code&gt;Claude Code&lt;/code&gt; 消耗的 &lt;code&gt;token&lt;/code&gt; 非常多(轻轻松松消耗上百万 &lt;code&gt;token&lt;/code&gt; 😭)&amp;lt;/span&amp;gt;, 如果只是用免费额度或使用按 &lt;code&gt;token&lt;/code&gt; 计费的服务, 需要在此页面时刻留意剩余的 &lt;code&gt;Tokens&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
或者使用付费的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt; 服务, &lt;code&gt;Lite&lt;/code&gt; 版本的按 &lt;code&gt;Prompt&lt;/code&gt; 计费, &lt;strong&gt;只需关注提问次数, 完全不用担心 &lt;code&gt;tokens&lt;/code&gt; 消耗量&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;前往 &amp;lt;a href=&quot;https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys&quot; target=&quot;_blank&quot;&amp;gt;设置页面&amp;lt;/a&amp;gt; 添加 &lt;code&gt;API Key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建 &lt;code&gt;~/.claude-code-router/config.json&lt;/code&gt; 文件, &lt;strong&gt;将模型添加到 &lt;code&gt;Providers&lt;/code&gt; 数组中&lt;/strong&gt;, 将第二步复制的 &lt;code&gt;API Key&lt;/code&gt; 填入 &lt;code&gt;Providers.api_key&lt;/code&gt;, &lt;strong&gt;并在 &lt;code&gt;Router&lt;/code&gt; 中指定模型路由&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;LOG&quot;: true,
  &quot;HOST&quot;: &quot;127.0.0.1&quot;,
  &quot;APIKEY&quot;: &quot;yourpassword&quot;,
  &quot;PORT&quot;: 65430,
  &quot;Providers&quot;: [
    {
      &quot;name&quot;: &quot;GLM&quot;,
      &quot;api_base_url&quot;: &quot;https://open.bigmodel.cn/api/paas/v4/chat/completions&quot;,
      &quot;api_key&quot;: &quot;aosiejflaJOIeifhwiofhjajsdf.sldfjaiOSioef&quot;,
      &quot;models&quot;: [
        &quot;glm-4.5&quot;,
        &quot;glm-4.5-air&quot;,
        &quot;glm-4-long&quot;,
        &quot;glm-4-flash&quot;
      ]
    }
  ],
  &quot;Router&quot;: {
    &quot;default&quot;: &quot;GLM,glm-4.5&quot;,
    &quot;background&quot;: &quot;GLM,glm-4.5-air&quot;,
    &quot;longContext&quot;: &quot;GLM,glm-4.5-air&quot;,
    &quot;longContextThreshold&quot;: 50000,
    &quot;webSearch&quot;: &quot;GLM,glm-4.5-air&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 Claude Code&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;claude-code-router&lt;/code&gt; 提供了 &lt;code&gt;ccr&lt;/code&gt; 命令来代理访问 &lt;code&gt;Claude Code&lt;/code&gt;, 但 &lt;strong&gt;直接使用 &lt;code&gt;ccr&lt;/code&gt; 会导致很多问题&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法在 &lt;code&gt;vscode&lt;/code&gt; 的 &lt;code&gt;Claude Code Plugin&lt;/code&gt; 中使用我们配置的模型&lt;/li&gt;
&lt;li&gt;也无法在命令行中直接使用 &lt;code&gt;claude&lt;/code&gt; 命令访问国产模型&lt;/li&gt;
&lt;li&gt;无法使用智谱的 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt; 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此我们要实现直接使用 &lt;code&gt;claude&lt;/code&gt; 命令使用 &lt;code&gt;Claude Code&lt;/code&gt;, 而不是使用 &lt;code&gt;ccr&lt;/code&gt; 命令:&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;~/.claude/settings.json&lt;/code&gt; 文件&lt;/p&gt;
&lt;h3&gt;使用免费的国产模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;http://127.0.0.1:65430&quot;,
    &quot;ANTHROPIC_API_KEY&quot;: &quot;yourpassword&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; 就是 &lt;code&gt;~/.claude-code-router/config.json&lt;/code&gt; 中的 &lt;code&gt;APIKEY&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;使用智谱的 GLM Coding Lite 服务&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;因为 &amp;lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot; target=&quot;_blank&quot;&amp;gt;GLM Coding Lite&amp;lt;/a&amp;gt; 使用了类似 &lt;code&gt;Claude Max&lt;/code&gt; / &lt;code&gt;Claude Pro&lt;/code&gt; 的包时服务, 所以配置与直接访问 &lt;code&gt;API&lt;/code&gt; 的配置不同&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;https://open.bigmodel.cn/api/anthropic&quot;,
    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;asoiejilfjJIOfejwiofji.OIsfjosiaejnlf&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt; 是智谱平台的 &lt;code&gt;API Key&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
如果已经启动了 &lt;code&gt;Claude Code&lt;/code&gt;, 每次修改配置文件后都需要执行 &lt;code&gt;claude restart&lt;/code&gt;(&lt;code&gt;ccr&lt;/code&gt; 对应 &lt;code&gt;ccr restart&lt;/code&gt; 命令)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;初始化&lt;/h2&gt;
&lt;p&gt;配置完成后就可以直接通过 &lt;code&gt;claude&lt;/code&gt; 命令启动 &lt;code&gt;Claude Code&lt;/code&gt; 了&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动 &lt;code&gt;Claude Code&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cd your-project
claude

╭──────────────────────────╮
│ ✻ Welcome to Claude Code │
╰──────────────────────────╯

 Let&apos;s get started.

 Choose the text style that looks best with your terminal:
 To change this later, run /theme

 ❯ 1. Dark mode✔
   2. Light mode
   3. Dark mode (colorblind-friendly)
   4. Light mode (colorblind-friendly)
   5. Dark mode (ANSI colors only)
   6. Light mode (ANSI colors only)


 Preview
 ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
 │   1   function greet() {                                                                                                        │
 │   2 -    console.log(&quot;Hello, World!&quot;);                                                                                          │
 │   2 +    console.log(&quot;Hello, Claude!&quot;);                                                                                         │
 │   3   }                                                                                                                         │
 ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是选择主题, 直接选择 &lt;code&gt;Dark mode&lt;/code&gt; 即可(按下 &lt;code&gt;Enter&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;如果执行结果是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;╭──────────────────────────╮
│ ✻ Welcome to Claude Code │
╰──────────────────────────╯

 Unable to connect to Anthropic services

 Failed to connect to console.anthropic.com: ERR_BAD_REQUEST

 Please check your internet connection and network settings.

 Note: Claude Code might not be available in your country. Check supported countries at https://anthropic.com/supported-countries
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则说明设置失败, 请检查配置文件格式是否正确, 以及是否已经设置了环境变量 &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; / &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt;, 如果已经设置了需要清除&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随后是一个安全说明&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;╭──────────────────────────╮
│ ✻ Welcome to Claude Code │
╰──────────────────────────╯

 Security notes:

  Claude can make mistakes
  You should always review Claude&apos;s responses, especially when
  running code.

  Due to prompt injection risks, only use it with code you trust
  For more details see:
  https://docs.anthropic.com/s/claude-code-security

 Press Enter to continue…
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接按 &lt;code&gt;Enter&lt;/code&gt; 继续&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置换行方式&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;╭──────────────────────────╮
│ ✻ Welcome to Claude Code │
╰──────────────────────────╯

 Use Claude Code&apos;s terminal setup?

 For the optimal coding experience, enable the recommended settings
 for your terminal: Shift+Enter for newlines

 ❯ 1. Yes, use recommended settings
   2. No, maybe later with /terminal-setup

 Enter to confirm · Esc to skip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接按 &lt;code&gt;Enter&lt;/code&gt; 继续&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安全风险提示&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                   │
│ Do you trust the files in this folder?                                                                                            │
│                                                                                                                                   │
│ /Users/xxx/projects/xxxxxxxxx                                                                                                     │
│                                                                                                                                   │
│ Claude Code may read, write, or execute files contained in this directory. This can pose security risks, so only use files from   │
│ trusted sources.                                                                                                                  │
│                                                                                                                                   │
│ Learn more ( https://docs.anthropic.com/s/claude-code-security )                                                                  │
│                                                                                                                                   │
│ ❯ 1. Yes, proceed                                                                                                                 │
│   2. No, exit                                                                                                                     │
│                                                                                                                                   │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Enter to confirm · Esc to exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里提示的是是否信任 &lt;code&gt;Claude Code&lt;/code&gt; 读取源码, 直接按 &lt;code&gt;Enter&lt;/code&gt; 继续&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入对话&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;╭───────────────────────────────────────────────────╮
│ ✻ Welcome to Claude Code!                         │
│                                                   │
│   /help for help, /status for your current setup  │
│                                                   │
│   cwd: /Users/xxx/projects/xxxxxxxxx              │
│                                                   │
│   ─────────────────────────────────────────────── │
│                                                   │
│   Overrides (via env):                            │
│                                                   │
│   • API Base URL:                                 │
│   https://open.bigmodel.cn/api/anthropic          │
╰───────────────────────────────────────────────────╯

 Tips for getting started:

  Run /init to create a CLAUDE.md file with instructions for Claude
  Use Claude to help with file analysis, editing, bash commands and git
  Be as specific as you would with another engineer for the best results
  ✔ Run /terminal-setup to set up terminal integration

╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ &amp;gt; Try &quot;write a test for &amp;lt;filepath&amp;gt;&quot;                                                                                               │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  ? for shortcuts                                    ✗ Auto-update failed · Try claude doctor or npm i -g @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;项目首次使用 &lt;code&gt;Claude Code&lt;/code&gt; 会提示执行 &lt;code&gt;/init&lt;/code&gt; 命令创建 &lt;code&gt;CLAUDE.md&lt;/code&gt; 文件, 这一步 &lt;code&gt;Claude Code&lt;/code&gt; 会扫描整个项目, 并进行代码分析和总结, 从而为大模型理解整个项目提供帮助&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/init


⏺ I&apos;ll analyze the codebase to understand its structure and create a comprehensive CLAUDE.md file.

⏺ Task(Analyze codebase structure)
  ⎿  total 48
     drwxr-xr-x  27 xxxxx  staff    864  9  5 10:13 .
     drwxr-xr-x  21 xxxxx  staff    672  9  5 10:13 ..
     … +25 lines (ctrl+r to expand)
     src/test/index.ts
     src/test/test/hasRole.ts
     src/test/test/hasPermi.ts
     … +17 lines (ctrl+r to expand)
     +12 more tool uses

· Puzzling… (esc to interrupt)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次执行扫描时会比较慢, 如果代码量过大, 也可能直接中断, 这里我就直接中断了:&lt;/p&gt;
&lt;p&gt;然后我又问了 &lt;code&gt;为什么没有生成 CLAUDE.md ?&lt;/code&gt;, 看上去它读取了部分文件, 然后生成出了 &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 为什么没有生成 CLAUDE.md ?

⏺ I&apos;ll analyze the codebase directly to create the CLAUDE.md file.

⏺ Read(package.json)
  ⎿  Read 142 lines (ctrl+r to expand)

⏺ Read(README.md)
  ⎿  Read 12 lines (ctrl+r to expand)

⏺ Search(pattern: &quot;*.md&quot;)
  ⎿  Found 1 file (ctrl+r to expand)

⏺ Read(vite.config.ts)
  ⎿  Read 162 lines (ctrl+r to expand)

⏺ Read(.eslintrc.cjs)
  ⎿  Read 52 lines (ctrl+r to expand)

⏺ Search(pattern: &quot;src/**&quot;)
  ⎿  Found 100 files (ctrl+r to expand)

⏺ Read(src/router/index.ts)
  ⎿  Read 8 lines (ctrl+r to expand)

⏺ Search(pattern: &quot;src/store/**&quot;)
  ⎿  Found 11 files (ctrl+r to expand)

⏺ Search(pattern: &quot;src/api/**&quot;)
  ⎿  Found 100 files (ctrl+r to expand)

⏺ Search(pattern: &quot;src/views/**&quot;)
  ⎿  Found 100 files (ctrl+r to expand)

⏺ Read(src/store/readme.md)
  ⎿  Read 57 lines (ctrl+r to expand)

⏺ Search(pattern: &quot;scripts/**&quot;)
  ⎿  Found 8 files (ctrl+r to expand)

⏺ Search(pattern: &quot;.cursor/**&quot;)
  ⎿  Found 0 files

⏺ Search(pattern: &quot;.cursorrules&quot;)
  ⎿  Found 0 files

⏺ Search(pattern: &quot;.github/copilot-instructions.md&quot;)
  ⎿  Found 0 files

⏺ Read(scripts/openapi-codegen.ts)
  ⎿  Read 17 lines (ctrl+r to expand)

⏺ Read(src/views/foo/bar/baz/index.vue)
  ⎿  Read 50 lines (ctrl+r to expand)

⏺ Write(CLAUDE.md)
  ⎿  User rejected write to CLAUDE.md
         1 +  # CLAUDE.md
         2 +
         3 + 这里我省略了 ...
       117   \ No newline at end of file

╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Overwrite file                                                                                                                    │
│ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ CLAUDE.md                                                                                                                     │ │
│ │                                                                                                                               │ │
│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ Do you want to overwrite CLAUDE.md?                                                                                               │
│ ❯ 1. Yes                                                                                                                          │
│   2. Yes, allow all edits during this session (shift+tab)                                                                         │
│   3. No, and tell Claude what to do differently (esc)                                                                             │
│                                                                                                                                   │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按下 &lt;code&gt;Enter&lt;/code&gt; 创建 &lt;code&gt;ClAUDE.md&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;context7 MCP&lt;/h2&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://context7.com/&quot; target=&quot;_blank&quot;&amp;gt;context7 MCP&amp;lt;/a&amp;gt; 能实时注入最新文档到某 &lt;code&gt;Claude&lt;/code&gt; 的上下文中, 解决模型知识的滞后性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;访问 &lt;a href=&quot;https://context7.com/dashboard&quot;&gt;Dashboard 页面&lt;/a&gt; 登录并创建 &lt;code&gt;API Key&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行命令安装 &lt;code&gt;context7&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add --transport http context7 https://mcp.context7.com/mcp --header &quot;CONTEXT7_API_KEY: ctx7sk-0iksefjl1-iwe2c-29n4-19nd-sliefoajn11&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;YOUR_API_KEY&lt;/code&gt; 替换为自己的 &lt;code&gt;API Key&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;vim mode&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 支持 &lt;code&gt;vim&lt;/code&gt; 模式(&lt;code&gt;vim&lt;/code&gt; 党狂喜 😁), 虽然只支持一些常用键&lt;/p&gt;
&lt;p&gt;可以通过执行 &lt;code&gt;/vim&lt;/code&gt; 启用, 或者 &lt;code&gt;/config&lt;/code&gt; 永久启用:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;输入 &lt;code&gt;/config&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ &amp;gt; /config                                                                                                                         │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  /config (theme)     Open config panel
  /agents             Manage agent configurations
  /hooks              Manage hook configurations for tool events
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;将光标移动到 &lt;code&gt;Editor mode&lt;/code&gt;, 按下 &lt;code&gt;Enter&lt;/code&gt;, 显示 &lt;code&gt;vim&lt;/code&gt; 后按 &lt;code&gt;Esc&lt;/code&gt; 退出&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Settings                                                                                                                          │
│ Configure Claude Code preferences                                                                                                 │
│                                                                                                                                   │
│ ❯ Auto-compact                              true                                                                                  │
│                                                                                                                                   │
│   Use todo list                             true                                                                                  │
│                                                                                                                                   │
│   Verbose output                            false                                                                                 │
│                                                                                                                                   │
│   Auto-updates                              true                                                                                  │
│                                                                                                                                   │
│   Theme                                     Dark mode                                                                             │
│                                                                                                                                   │
│   Notifications                             Auto                                                                                  │
│                                                                                                                                   │
│   Output style                              default                                                                               │
│                                                                                                                                   │
│   Editor mode                               vim                                                                                   │
│                                                                                                                                   │
│   Model                                     Default (recommended)                                                                 │
│                                                                                                                                   │
│   Auto-connect to IDE (external terminal)   false                                                                                 │
│                                                                                                                                   │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;vscode 插件&lt;/h2&gt;
&lt;p&gt;可以直接在 vscode 应用市场中搜索并安装 &lt;code&gt;Claude Code&lt;/code&gt; 插件, 点击右上角的 &lt;code&gt;Claude Code&lt;/code&gt; 图标即可启动&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装插件后, 点击右上角的 ❄️ &lt;code&gt;Claude Code&lt;/code&gt; 插件图标, 显示如下界面
&lt;img src=&quot;./assets/images/claude-code-vscode-plugin-error.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是因为插件 &lt;strong&gt;默认使用 &lt;code&gt;Anthropic&lt;/code&gt; 官方的 &lt;code&gt;Claude&lt;/code&gt; 系列模型&lt;/strong&gt;, 我们需要修改插件的配置:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在左侧插件列表中搜索 &lt;code&gt;Claude Code&lt;/code&gt;, 然后找到 &lt;code&gt;Claude Code for VS Code&lt;/code&gt;, 点击 ⚙️ 按钮, 点击设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-vscode-plugin-error2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入设置页面, 我们需要修改的是 &lt;code&gt;Environment Variables&lt;/code&gt;, 点击 &lt;code&gt;在 settings.json 中编辑&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-vscode-plugin-error3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;settings.json&lt;/code&gt; 中增加环境变量配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{
+  &quot;claude-code.environmentVariables&quot;: [
+    {
+        &quot;name&quot;: &quot;ANTHROPIC_BASE_URL&quot;,
+        &quot;value&quot;: &quot;https://open.bigmodel.cn/api/anthropic&quot;
+    },
+    {
+        &quot;name&quot;: &quot;ANTHROPIC_AUTH_TOKEN&quot;,
+        &quot;value&quot;: &quot;asoiejilfjJIOfejwiofji.OIsfjosiaejnlf&quot;
+    },
+  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;请将 &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt; 对应的 &lt;code&gt;value&lt;/code&gt;, 替换为自己的 &lt;code&gt;api key&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;关闭 &lt;code&gt;Claude Code&lt;/code&gt; 窗口, 再次点击 ❄️ &lt;code&gt;Claude Code&lt;/code&gt; 插件图标, 此时就可以正常进行对话了&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/claude-code-vscode-plugin-error4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Error Invalid API key&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; hello ?
⎿  Invalid API key · Please run /login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用 &lt;code&gt;GLM&lt;/code&gt; 时, 如果遇到了 &lt;code&gt;Invalid API key&lt;/code&gt; 错误, 说明 &lt;code&gt;Claude Code&lt;/code&gt; 并没有读取到你配置的 &lt;code&gt;API Key&lt;/code&gt;, 应该检查以下配置文件:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;./.claude/settings.local.json&lt;/code&gt;: 项目内的配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.claude/settings.local.json&lt;/code&gt;: 全局配置文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Claude Code&lt;/code&gt; 在使用时, 可能会将项目的配置写入 &lt;code&gt;./.claude/settings.local.json&lt;/code&gt;, 也会 &lt;strong&gt;会优先读取 项目内的配置文件&lt;/strong&gt;, 所以如果有项目内的配置文件, 那么我们 &lt;strong&gt;也要在这个文件内配置 &lt;code&gt;API Key&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;permissions&quot;: {
    &quot;allow&quot;: [
      &quot;Bash(mkdir:*)&quot;,
      &quot;Read(//Users/xxx/projects/xxx/src/)&quot;,
    ],
    &quot;deny&quot;: [
      &quot;Bash(pnpm lint:*)&quot;
    ],
    &quot;ask&quot;: []
  },
  &quot;env&quot;: {
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;https://open.bigmodel.cn/api/anthropic&quot;,
    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;asiehjfioahgeriaofhjo23i.aiosjfgoiaeijfjio&quot;,
    &quot;API_TIMEOUT_MS&quot;: &quot;3000000&quot;,
    &quot;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC&quot;: &quot;1&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ccusage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ccusage&lt;/code&gt; 是一个用于统计 &lt;code&gt;Claude Code&lt;/code&gt; 使用量的工具, 它从 &lt;code&gt;~/.claude/projects&lt;/code&gt; 目录读取了大模型的对话历史记录, 从而统计每天的 &lt;code&gt;tokens&lt;/code&gt; 消耗量&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装 ccusage&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pnpm i -g ccusage
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;执行 &lt;code&gt;ccusage daily&lt;/code&gt; 查看每天的 &lt;code&gt;tokens&lt;/code&gt; 消耗量&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ccusage daily

 WARN  Fetching latest model pricing from LiteLLM...                                                               ccusage 15:18:05

ℹ Loaded pricing for 1491 models                                                                                  ccusage 15:18:05

 ╭──────────────────────────────────────────╮
 │                                          │
 │  Claude Code Token Usage Report - Daily  │
 │                                          │
 ╰──────────────────────────────────────────╯

┌────────────┬───────────────┬────────────┬───────────┬───────────────┬─────────────┬───────────────┬─────────────┐
│ Date       │ Models        │      Input │    Output │  Cache Create │  Cache Read │  Total Tokens │  Cost (USD) │
├────────────┼───────────────┼────────────┼───────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2025-09-08 │ - sonnet-4    │  1,038,167 │   100,264 │             0 │   6,720,666 │     7,859,097 │       $6.63 │
├────────────┼───────────────┼────────────┼───────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ 2025-09-09 │ - sonnet-4    │  1,319,907 │    73,242 │             0 │  24,117,626 │    25,510,775 │      $12.29 │
├────────────┼───────────────┼────────────┼───────────┼───────────────┼─────────────┼───────────────┼─────────────┤
│ Total      │               │  2,358,074 │   173,506 │             0 │  30,838,292 │    33,369,872 │      $18.93 │
└────────────┴───────────────┴────────────┴───────────┴───────────────┴─────────────┴───────────────┴─────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://guangzhengli.com/blog/zh/vibe-coding-and-context-coding&quot;&gt;谈谈 AI 编程工具的进化与 Vibe Coding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7543530064106602547&quot;&gt;看这一篇就够了！Claude Code 接入四大国产编程模型 DeepSeek、GLM、Qwen、Kimi 全指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/hJj13IDO4ysiGx_cPldwjg&quot;&gt;3 分钟讲透 Win 版 Claude Code部署：整合 Qwen-Coder + GLM-4.5，零翻墙平替 Cursor！&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/musistudio/claude-code-router/blob/main/README_zh.md&quot;&gt;claude-code-router 中文文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bigmodel.cn/claude-code?cc=fission_glmcode_sub_v1&amp;amp;ic=Q2N8XA4W77&amp;amp;n=a****3&quot;&gt;GLM Coding Lite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.bigmodel.cn/cn/guide/develop/claude&quot;&gt;接入 Claude Code - GLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/musistudio/claude-code-router/issues/427#issuecomment-3166642325&quot;&gt;#3166642325&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://context7.com/&quot;&gt;context7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>济你太美, 徐度光阴(超多图)</title><link>http://blog.xiaban.run/posts/2025/2025-jining/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/2025-jining/</guid><description>因工作变动, 我从 6 月份开始前往济宁出差, 开始了一场被无限拉长的工作经历</description><pubDate>Sun, 20 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;图为孔子和他的学生, 拍摄地为 &amp;lt;a href=&quot;https://surl.amap.com/b1pnF3z1q5zD&quot; target=&quot;_blank&quot;&amp;gt;济宁市博物馆文化中心馆&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;逝者如斯夫, 不舍昼夜&lt;/strong&gt;&lt;/em&gt;, 意为 &lt;strong&gt;时间像流水一样, 日夜不停地流逝&lt;/strong&gt;, 这句话表达了孔子对时间流逝的感叹，也蕴含着珍惜时间，不虚度光阴的哲理&lt;/p&gt;
&lt;h2&gt;工作变动&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;人生总是充满了变数&lt;/strong&gt;, 因工作变动, 我从 &lt;code&gt;6&lt;/code&gt; 月份开始前往济宁出差, 开始了一场 &lt;strong&gt;魔幻的被无限拉长的出差经历&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;6&lt;/code&gt; 月份时得到消息: 干到 &lt;code&gt;6&lt;/code&gt; 月底就回原工作地&lt;/li&gt;
&lt;li&gt;&lt;code&gt;6&lt;/code&gt; 月中旬得知: 没希望了, 无法回来&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt; 月初得知: 应该稳了, 可以回来&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt; 月初又得知: 双方领导层正在协商, 可以回来&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt; 月中旬又得知: &lt;code&gt;8&lt;/code&gt; 月份就可以回原工作地&lt;/li&gt;
&lt;li&gt;&lt;code&gt;8&lt;/code&gt; 月初得知: 正在确认细节&lt;/li&gt;
&lt;li&gt;&lt;code&gt;8&lt;/code&gt; 月初又得知: 还在走流程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;8&lt;/code&gt; 中旬得知: 即将返回原工作地&lt;/li&gt;
&lt;li&gt;&lt;code&gt;8&lt;/code&gt; 中旬: 返程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从商业谈判的角度来看, 这其中发生的故事肯定非常精彩, 但对于真正的劳动力来说, 这可一点都不好玩&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-shawshank-words-1.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-shawshank-words-2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;希望是美好的，也许是人间至善，而美好的事物永不消逝。&lt;br /&gt;
Hope is a good thing, maybe the best of things, and no good thing ever dies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这趟旅程像极了 &amp;lt;a href=&quot;https://www.bilibili.com/bangumi/play/ep284310&quot; target=&quot;_blank&quot;&amp;gt;🎬 肖申克的救赎&amp;lt;/a&amp;gt;, 在一次次希望与绝望之间等待最终的救赎, 我们或许未必经历过监狱, 但谁没有被生活的高墙围堵过? 唯独美好的事物永不消逝&lt;/p&gt;
&lt;h2&gt;出发&lt;/h2&gt;
&lt;p&gt;由于我的行李比较多(一个大件行李, 一个行李箱, 一个电脑包), 所以打算开车去&lt;/p&gt;
&lt;p&gt;经过 &lt;code&gt;4&lt;/code&gt; 个多小时最终到达济宁的出租屋(公司提供的宿舍), &lt;strong&gt;这是一个三居室, 但只有一个卧室有空调, 另一个空调在客厅&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-living-room.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为客厅(打扫后), 我们安顿好之后立马出去买了拖把, 把洗手间和客厅打扫了一遍(简直太脏了 😫)&lt;/p&gt;
&lt;p&gt;右侧为厨房, 厨房非常的脏, 一开门全是土, 里面堆着面和油, 没有招来老鼠蟑螂也是个奇迹&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-bedroom.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为唯一一个有空调的卧室(我的卧室), 在酷热的夏天能有一台空调续命真的太舒服了, 我宣布 &lt;strong&gt;空调是最伟大的发明&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-bedroom-door.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为卧室门, 据说之前是小孩子的卧室, 这种标语对小孩子应该会有激励作用, 但对我们来说...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果努力就可以成功, 这个世界的首富应该是我们村里的驴&lt;/li&gt;
&lt;li&gt;应该努力让自己成为自己想成为的人, 而不是活成别人希望的样子&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;济你太美&lt;/h2&gt;
&lt;p&gt;之前从未来过济宁, 对济宁的认知还停留在 &lt;strong&gt;孔孟之乡&lt;/strong&gt; 和 &lt;strong&gt;&amp;lt;a href=&quot;https://www.douyin.com/user/MS4wLjABAAAAoC6BLDETRW9UFXZ4p4Y-_lKwXWrImjkRYZDxIXs3G3HjYQDAcmWTGUGHUja8K-O-&quot; target=&quot;_blank&quot;&amp;gt;汤姆张&amp;lt;/a&amp;gt;&lt;/strong&gt; 上, 没想到搜了一下济宁, 意外的发现了济宁文旅竟然在蹭哥哥的热度, 济宁居然真的有 &amp;lt;a href=&quot;https://surl.amap.com/gBriRHn1aH6&quot; target=&quot;_blank&quot;&amp;gt;蔡徐村&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/jining-kunkun.mp4&quot; type=&quot;video/quicktime&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;视频作者为 &amp;lt;a href=&quot;https://www.douyin.com/user/MS4wLjABAAAAKwBGPrZNyFY1CnnFd60nmzQ1b_Ziprh-BE7adF0uNqo&quot; target=&quot;_blank&quot;&amp;gt;济宁你好&amp;lt;/a&amp;gt;, 看起来并不是网传的由济宁文旅制作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-caixucun.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击查看 &amp;lt;a href=&quot;https://surl.amap.com/gBriRHn1aH6&quot; target=&quot;_blank&quot;&amp;gt;蔡徐村&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;初来乍到&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-residential-area-sky.jpg&quot; alt=&quot;&quot; /&gt;
图为刚开始下雨时的天空, 在小区内拍摄, 深色的乌云像水墨一样逐渐晕染开来&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-bus.jpg&quot; alt=&quot;&quot; /&gt;
图为济宁的公交车, 印有 &quot;情系万里, 德行济宁&quot;, 据说早晚高峰免费, 但是来了这么久没坐过公交&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-railway-station.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为济宁北站出站口的展示牌, &lt;em&gt;青年发展友好型城市&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;但现实情况是, 疯狂宣传的往往是有所欠缺的, 根据济宁市统计局的数据, 2024年相比于2023年, 济宁市常住人口减少了 5.32 万人(824.05 - 818.73), 降幅创近十年新高, 详见 &amp;lt;a href=&quot;https://www.sohu.com/a/892879874_121123761&quot;&amp;gt;济宁及各县市区最新常住人口数据公布&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;我觉得应该改成生活节奏友好型城市, 这是一座生活节奏比较慢的城市, 大家都比较随性, 有生活气息, 人与人之间基本没有距离感&lt;/p&gt;
&lt;h3&gt;散装的济宁&lt;/h3&gt;
&lt;p&gt;济宁市区在任城, 济宁的其他下辖地市都比任城知名度高, 例如 曲阜 / 邹城 / 兖州 / 梁山&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;机场也在嘉祥, 没错, 济宁的机场叫做曲阜国际机场, 不在曲阜也不在城区, 而在嘉祥&lt;/li&gt;
&lt;li&gt;京沪高铁也没有经过城区, 而是在曲阜设有曲阜东站
任城与兖州邹城曲阜之间又有成片的煤矿塌陷区, 无法连片发展&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-coal-mine-subsidence-area.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;更加令人疑惑的是, 主城区四处建设新区, 最后哪都没发展起来:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;东边建立了高新区, 但因为煤矿大量开采, 土地塌陷越来越严重, 根本不适合开发&lt;/li&gt;
&lt;li&gt;西边建立了经开区, 往西向距离较近的嘉祥发展, 但同样没有发展起来&lt;/li&gt;
&lt;li&gt;南边建立了太白湖新区, 但太白湖往南是南阳湖, 根本没有发展的空间&lt;/li&gt;
&lt;li&gt;把高铁站(济宁北站)放到了距离城区很远的北边&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;煤矿塌陷区&lt;/h3&gt;
&lt;p&gt;为什么济宁的城市规划如此散装? 因为在济宁 &lt;strong&gt;分布着数量众多的煤矿开采区和塌陷区&lt;/strong&gt;, 因为煤矿塌陷区的阻隔, 各辖区无法连片发展, 济宁也正在积极治理因煤矿开采带来的诸多问题&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=616189435&amp;amp;bvid=BV1ph4y1L7d9&amp;amp;cid=1202992399&amp;amp;p=1&amp;amp;autoplay=0&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;工作&lt;/h3&gt;
&lt;p&gt;这个项目是老系统升级改造, 即参考原系统代码在新系统中实现, 没有让我们评估工时, 而且工期非常紧，所以一直加班&lt;/p&gt;
&lt;h4&gt;老系统升级&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-iceberg.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这又是一个充满槽点的话题, &lt;strong&gt;限于篇幅, 以后有时间会单独写一篇文章&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于原项目是 vue2, 新项目是 vue3, 所以为了代码迁移时少掉一些头发, 我还特地尝试了基于 gogocode 的 &amp;lt;a href=&quot;https://sublimect.github.io/gogocode-playground/#code/N4IglgdgDgrgLgYQPYBMCmIBcIQBoQDuSATgNbLpYgBmMEAxnGEhAARzECGEAztSQFsAFNTAAbNAEkI-XK05QwcpFCYseASlbAAOm1b11cVgBJWAXnmKAdAHMk9w+j2sDR1jyQxi9NBdaiEtL81p7evi5uvMacPMaWJkJhPmhyuvquUJzEPGgA8qrMvJjaka6uyb4AKgCeUGglAOQCqDASjbhl5WLctjCctg2sjQBuMGgdXZliMLaQPCUA2o1wdWg89MRgqo0Aup0ZrAC+B65HGnqRAPRXrMRocN5sNV7E7Fy8-MQCaChu6KwABZoe6RQzRdhoARQHpwPyWWJwayiCAoISNAA8cOhsLQAD4MVdsTDOHC8Y0LvpwXEPJttvF5HFkZA0ZiNltVASruz6eTKddblioSSyaxABcJgDAlSKA7goCRVYW4oTE3H8qnuKDEFQ8fyLXYC1gYnmc8VS-Qy1ESADKdNUSVtcDkmu1atcEmMTnhjKRgwgINJaCErtYN1YgG-PQAmaaw0AAPKAkYzoaicNrGYBHViAKeVAG6KgFDFMWAELdAOgJgHflMGoL33FojNAAUTjCYAImhk6mhJ7Ka5Pf5OCgUABBCA1KpIABidEYRQACtlOAIeO2K53-l7PdZ7iTfOijQ7yXI2Q6PA8YFBWD0ILZzDoQHAeNe+Zd9PdHsQ2J69EdH7QGGo2FWkDW9bxsQcDNq2YhwIu6BaOkrjPk8K5dOuaCboGVzUnAOg6DwADUQiLJwAC0ABe-aEQAWgA+iYuw4Rc2EAFRXox9zUFhQj4Vhd46FauwMQA-PRGi4Fctj7hhpgAIz+KxQgmAATBoADcFJIRuPRblcQhYQARt+k4sLpXEMQRJFkVRNGLFhBA0QxxnsUI-GYIsAB6QbprsAA+7GuUGfFYRoGh2To9E8MFwBWdhWG8QJ9FXFhwV8GxUU6MF8U6J+jEiWJwwmJJWEQEcqmHMhqFCFcsbAZh2E4UmKYQfZOi6IxVz7gVYDQgm2h3C2ygQAAsl4EBwigciGNC8C-McARagIrBYaM4wLQVxXlKwpUaWhWFHEp-HGY55gYlh3K7horXDKt5QbZwmnHQlXHUMl3Fpdl+4qcurjXbdOjpYlj1cfdP3bQFr3DOc71qShm3lXdqUPU9gM3DljSXZ96k3WhlFwICYDcdYon7tjuPWKj63o5pIzZFxWM4zwBMXR9ZNQxj5VCJTxCee6nkYaFDE07jxnMWFRM8Lt9Mo4zX2YyLgvGSLYvI6TUvlTLOjWJxOgoNZWFYZRtH0eVyN5eikvk2hqvq5FWs6AQOs6HrdHi3l1iUzMEym8zmkW1hJhQmA1WG-ufuQUrZsq7TWHWD7rF4xr1sEPrTuSS7nBu6HnvmxHavRy23GLMs167HH1n6wXID6jouxJynace2VVyYJgIyEegKHaYxGhCNYDG88A4uYK3UByYp2jp-XFsmOCojfOx4u1mI-XrDwAxoAAQkgMbWFPYDfCbH5fhOv5ArK8qKgGypn3CMEGmgYiETp8BwCw7BrFeN6xphICsIAAOmAIGRgA28YAI2NABcctGO+D84BPzYKseob9NQdWyDUa8Z5ICkEAEAMkQVQBmVl0TEt976P2fjAtAb84Qxk-qYEwJgAAMeIqF5UJPgiBUC9y4IxEwwh0DX7XngQIRByCxCoMoTQuhVDJKMPAZw8kkRlxYLhDgw4eC74oDAKnBwrAm64zADpCQoQagMDfiYaiyD6G0PoeIiqyjVFiAcKwxR7CrFqNsKwZuLR0BiEMcYr+pjREMMsS3axtjGgyP3hAPQ+kj4WjlGgG0HJILGkdKwZ0UBNClH0KGBJytGiVS6nVVM2h6FFX3PQikSlywQm7JYTJKJWQ5JAqwPJEEClUKKspcszgSph0aONeMfphoLHTB0BmkMyrNDADGeYzldhDIliM6GjQIBziGEYkwMyx7zMCHCHIJRBn7lDFUPIjY8isA6jCKEaB+kBHEFsngl13ydIzuiZJAzCkzP2Yc459hHAVlYIAA9NACrNoAEE1ABgLoAGV1ADqmoAT+1ACGMXIQAAkaAFO5P+kLAAr1oABLtADHcoAQA9ABXgYAGQjAD-ZoAgqElnnmCTJANAU4tQpKEMAQpZ0GIBGoKwBiVw7kViQjU9EKBSScCDNoVpXKWTongq+QVpNQxZJWXJaiMEVlFOGBJFZ-hZUmBKNwGoWhzB4gKdRIVDzRkrMwCsmZyrqIyRbGqjQ6yWaNDFWwelLSZklOXGuLpvK4D8vlc64pVCOUdLWsyVE6IemTRQJgQZdd5kyo0E6kwirunuBVZYMNI0OLat1c1CA9CCpXCZY9VlVxzi2q3I0awFa+FQCtF6uEckqE2v3KWwMSaJojR2a8v1qy3WcpKtyxoBBST0EBJGg1QaunGuAFEiQxBrXxqOImwdcBh0Zp1Ss3A1qdXZtzR+G10a7WTunSCdyhTF1DsBKuuh1EN0aC3QVHdhU91zJZlMeaIBGimsjUe2dKy42FNwIPTAHBxifjwK+gd57L3rs3XieduBgCAeA2gEtXR91blfdeD91FGhfpPse3987QOnDWsMJdK6gxruvTB7dVCCoocOGhwMGGQA6xACs1juHLT4blXBxDxAQPXmI2tCDy6L0UavSYDdv6t3-oQ2gFCQH+PIZtah596HDiuGvKx9jWEsBTrwz+njp7BPgbI2J29EmpNyrvXoB99G1qMfRDKgjnbSOQfE9B39Fgs33to7u5tTnqInt9W50TUGqOZtKLZvzj6AvBLY14udrmRPkci55uV3mos5piyWxzwSdBYZMAtYLCaZlmfC5JgVOqst2afYa+Z152NYACIfIorAkshZS+ZzLKqrMmEizRor-m8sLR0zhlrP4iglbPWFjzEXMuDbo3V8djyuuRuS669paBhUhuaA8QEqABljqumHcDsR9H0FMEF0xf6xFFLOzwC7E2DJsGc1Q6hPq8p5vzcyotRVVP1YPdd97t28qJoiW1t7NDr7Ra+3oH7ha2XHbRqt87DAruJFB4mtHl2IfP1jTVmLCOWVI7i7G+dMy8evaC59vNBaSfFriz8bGh31udc2+qQNJ3VubDQAGNEWOZksFXi2EgaBBp0EguJxbw21MtpaJL3402hcDSGum6XvmhsQGJ395b3PRk6VF-cAAqhABXw1lf7mF0btApvzdS4szL2LeW6D26V4Lq3EA7dq6Vxr2HS24uG6+GgZscQtQ1Et8Ma3wfveS8vU73LcueXrCfsQGo7uKee9j8NX3jvNcB+XFtpPKxabWBMD8Hgy9Bhl+u3KmZ89F6V5XjXxIv7iqfm1wxOybAmXAMAL+KgAHU0AFnagBJOQLIAIeUB+AG-bQABUqsAAAYaqHPP1ggBvH0ANHqgAIf8ALBygAseRBYPwAwDGAApXQAkP8uCZQAASyFwOa3ZAD5yoAac1WC1HqLE+krBsUX9YJfh1OoiWABwKgWF-j3vFDIK1s-L2AOEOCOOOJNiwDOLfguB2GknBMeOKu6o8l0KGIAKNygAnMqACyic9kfKPpvoAO6KgAsCqABoRoABYRgAffGADw+qfoADD-gAu-KACD0QAYAEbpgA9GaoHlBaS6RU66SOSYBcQ4SRQ2T6z8TeQ6B6QQFhJyG8wOSuQaB8RCSNR9xiRdBCCUROisTjJyBcAECIFzipLVawQkYSQnieq-CmHzgyScAmGzjzivqfQ8Awj+zoi4CkxBpVpCA35zgLZuHlASQcAdQ-B-CWCBECDWDhHCCMwkYnIspCAACE8RkRWgDq7wERvwj4SRa0oYgAz7GADj8YAE+6gAZHob4hisCADHkYACHmNR9wNIk+IRrgYAKRGRvw1gkA9AMw6AC4OGFIrAnknkIYLkiwzUeo3cVwcRKeyoWwAgmRIxYxXRKAoQXqIEPAAA6v7BeuWhWhSNfBpgUdkWsW0ccPkQUa4KGIAJDmgAG3mAAl0YADwKgAWP+ABjRoAAhGrAiyPwrAgAMSpPHPEXEdHtZrE9EMD9HrDojmBHH8HXFdjuCLCkBoA1ByBJi7D+DgkeGCIhywkaDWD+E6jVY8BxGLFBiJGnHoFsDz4mDAAok1BHBL41D+B0lJhHDz4XEd4XGhi-JAnMGAAvboAKXGM+G+Fx2RtJwAaxTJ8gy+IR5wIR1gAAVkgJAN4cMMGGtBKXSZqC2OMkcHJMADYfzvYTwOcNoJyYcB3q4JSB3ngOANAPAAADK9BUBjAYD4Ayg8DUoPBMAghYDJhiC5D4A8AwA6QABqYAaABAr+GA2APS2QGARwQAA&quot; target=&quot;_blank&quot;&amp;gt;🔗 迁移脚本&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;用于将 &lt;code&gt;vue2&lt;/code&gt; 转换为 &lt;code&gt;composition API&lt;/code&gt; 的 &lt;code&gt;vue3&lt;/code&gt; 代码, 由于时间关系, 脚本只是个半成品, 并不完美&lt;/p&gt;
&lt;h4&gt;工作体验&lt;/h4&gt;
&lt;p&gt;程序员加班或许已经是行业共识，大家好像也习以为常，总是被迫愿意用更多的工作时间来换取项目的尽快交付，经历了两个月高强度的加班，让我深切的感受到: &lt;strong&gt;软件行业是劳动密集型产业&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们看似投入了时间，精力，想法，产出的是某些系统或功能模块。实际上我们投入的是敲击键盘鼠标，得到的是工资到账。因为在老板眼里，你与工地搬砖的工人和村里种地的农民无异，&lt;strong&gt;我们在用脑力搬砖或者挥舞锄头&lt;/strong&gt;，所以程序员群体都戏称自己是码农。&lt;/p&gt;
&lt;h4&gt;搬砖?&lt;/h4&gt;
&lt;p&gt;可是我们所做的工作明明是有技术含量的，为什么会有搬砖的感觉呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-bricklaying.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为程序员在角色分工中就是最基层的制造者&lt;/p&gt;
&lt;p&gt;程序员不像 画家/ 设计师, 程序员的工作整体分为系统设计和编码两部分:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统设计: 分析需求并找到可行的实现方案&lt;/li&gt;
&lt;li&gt;编码: 根据实现方案编写代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在系统设计阶段,我们做的是创造性的工作, 这一阶段很像画家作画, 设计师作图, 从零到一找到解决方案; 第二阶段编码更像是搭积木, 根据自己设计的图纸一步一步搭建自己的城堡, 在编码时应该是按图索骥水到渠成的, 看起来很美好对不对?
实际上系统设计阶段所花费的时间和精力往往被低估, 需要验证可行性, 技术选型, 需求分析, 与产品经理反复确认需求
再说编码阶段, 如果时间充足, 编码过程是很美好的, 但如果工期很紧, 就是截然不同的工作体验了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;赶进度 = 以最快的方式实现功能 = 牺牲代码质量 = 写屎山代码 = 增加维护成本&lt;/li&gt;
&lt;li&gt;人的工作效率是会随着连续工作时间增长而下降的, 加班时间越多, 注意力越难以集中, 作为碳基生物感到愈发疲惫&lt;/li&gt;
&lt;li&gt;工作没有正向反馈, 只有负面反馈, 非程序员难以理解开发中的坑, 就像长安的荔枝中的李善德, 一路跋山涉水快马加鞭把荔枝运到了长安, 无人在意途中发生了什么, 这只是本该送到的普通水果罢了, 博得妃子笑是因为荔枝本身香甜可口,荔枝不新鲜了或者没送到才是意外&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;加班&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-overtime-work.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到加班这件事上, &lt;strong&gt;到底为什么程序员群体会普遍加班呢?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工时评估时迷之乐观&lt;/li&gt;
&lt;li&gt;低估系统迁移工作量, 忽略原系统复杂度和代码量&lt;/li&gt;
&lt;li&gt;需求不明确或没有分析透彻, 只看到了整体需求的冰山一角&lt;/li&gt;
&lt;li&gt;公司崇尚加班文化 / 内卷文化 / 能者多劳文化&lt;/li&gt;
&lt;li&gt;低估了 debug / 联调 / 系统设计 所需的时间&lt;/li&gt;
&lt;li&gt;所做的功能模块缺少数据或者需要等待其他人开发完前置功能&lt;/li&gt;
&lt;li&gt;过于相信队友, 高估了队友的能力&lt;/li&gt;
&lt;li&gt;拥有一个不懂技术又喜欢指导一切的领导&lt;/li&gt;
&lt;li&gt;团队内缺少专业的产品经理和测试角色&lt;/li&gt;
&lt;li&gt;忽略屎山项目的代码阅读成本&lt;/li&gt;
&lt;li&gt;忽略前期项目环境搭建和配置的耗时&lt;/li&gt;
&lt;li&gt;低估了需求变动导致的额外工作量&lt;/li&gt;
&lt;li&gt;忽视文档编写 / 参加会议浪费的时间&lt;/li&gt;
&lt;li&gt;莫名其妙的机器思维, 认为人可以像机器一样全神贯注地每天运转至少8小时&lt;/li&gt;
&lt;li&gt;项目缩减工期或要求提前完成任务&lt;/li&gt;
&lt;li&gt;需求频繁变动, 把新需求当成 bug&lt;/li&gt;
&lt;li&gt;工作不被认可影响心情, 用代码量评估工作量, 用 bug 数量确定工作能力, 就像用硬盘大小评估一台电脑的性能, 用发热量作为所有硬件的唯一评估标准&lt;/li&gt;
&lt;li&gt;拥有一台卡到想砸烂的电脑, 严重影响编码体验和开发速度&lt;/li&gt;
&lt;li&gt;程序的世界中总是充满了各种稀奇古怪的问题, 你总会花时间解决你根本无法预料的报错, 如果遇到了解决不了的问题, 还可能调整方案或推翻设计方案重新设计&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;内卷&lt;/h4&gt;
&lt;p&gt;如今就业环境非常恶劣, 一方面劳动力越来越多, 另一方面就业岗位越来越少, 而且降本增效之风已经蔓延到了各行各业, &lt;strong&gt;我们正在陷入一个挣扎的内卷时代, 在这个内卷旋涡中没有人可以独善其身&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;剧场效应&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-theater-effect.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在一个剧院里，大家都在看戏，突然前排有个观众站起来了，周围的人劝他坐下，他置若罔闻，求助剧场管理员，管理员却不在岗位。&lt;/p&gt;
&lt;p&gt;于是逐渐整个剧院的人都站起来了，&lt;strong&gt;大家看到的是跟之前一样的视角，但每个人都更累了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更悲剧的是，虽然大家都叫苦叫累，&lt;strong&gt;但不会有人选择坐下来，因为坐下就真的什么都看不到了。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;被硅基生物取代?&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-robot.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;ChatGPT&lt;/code&gt; 诞生之前, 应该很少有人会考虑这个问题, 但在 &lt;code&gt;AI&lt;/code&gt; 具备越来越强大的工具属性, 甚至能嗅到 &lt;code&gt;AGI&lt;/code&gt; 的意味之后, 这个问题的答案仿佛无限趋向于肯定&lt;/p&gt;
&lt;p&gt;碳基生物最大的缺点就是饿了要吃饭, 困了要睡觉, 如果有一种程序员可以不知疲倦的 &lt;code&gt;24h&lt;/code&gt; 敲代码, 没有性格缺陷, 听从指挥任劳任怨, 那老板们岂不是做梦都要笑醒?&lt;/p&gt;
&lt;p&gt;在可以预见的未来, AI 会取代相当一部分程序员岗位, 这取决于 AI 进化的速度&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;限于篇幅, 以后有时间会单独写一篇文章&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;吃喝&lt;/h3&gt;
&lt;h4&gt;甏肉&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-bengrou-ganfan1.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-bengrou-ganfan2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为济宁甏肉, 口感软糯肥而不腻, 这不就是把子肉吗, 为什么叫甏肉？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;甏：一种盛放食物的器皿&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在北宋年间，这里的水泊梁山就成了好汉们的聚义地。随着梁山名气渐大，投奔而来的英雄剧增，普通炊具做饭量少愈慢。厨房伙计无奈之下用腌咸菜的大甏（beng，四声）充当炊具。把大块五花肉放在甏中，加老汤，填葱姜，佐料适中，将甏置于挖好的坑中，用干柴引火，木炭燃料，慢火细炖，逐渐甏内如玛瑙般微泡四起，肉香扑鼻，碳白火暗。取大勺每人满碗，大块肉放入嘴中，闭口间，肉已滑入肚内，爽滑至极，却不油、不腻。饮大坛米酒，顿觉满口生津，荡气回肠。虽不是山珍美味，也没有精挑细选，然大块吃肉、大口喝酒，已是赛似神仙般逍遥快活，越发彰显出梁山英雄的豪迈气概。&lt;/p&gt;
&lt;h4&gt;葡萄鸡丁&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-diced-chicken-with-grapes.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为葡萄鸡丁, 黑暗料理, 口味偏甜, 吃不惯, 好好的葡萄为什么要炒它?&lt;/p&gt;
&lt;h4&gt;醋溜肉丝&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-shredded-pork-in-vinegar.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;口感不错, 很合胃口&lt;/p&gt;
&lt;h4&gt;大盘面条鸡&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-big-plate-chicken.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;真的好大一盘, 桌子都显得小了, 底下还有面条&lt;/p&gt;
&lt;h4&gt;四孔鲤鱼&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-carp.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为四孔鲤鱼, 就是有四个鼻孔的鱼, 其实味道跟普通的鲤鱼差不多&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在微山湖周围，有着丰富的关于微山湖鲤鱼的掌故和传说。孔圣人的夫人生了孩子，鲁国国君专派人送了一条活鲤鱼祝贺，孔子因此给儿子取名孔鲤，可见孔子对鲤鱼的喜爱。明朝正德皇帝沿京杭运河南巡，住在微山湖南岸庙道口宋家楼，在李翠莲开的酒肆里吃到了李翠莲做的湖水炖湖鱼（鲤鱼），龙颜大悦，与年轻美貌的李大姐演绎了一场浪漫佳话，在当地传诵至今。乾隆皇帝游江南，亦过微山湖，也吃到了微山湖鲤鱼，赞赏之余，又把微山湖 鲤鱼封为贡品，从此微山湖鲤鱼年年上到皇宫龙宴。明末清初，微山湖西岸出了一个大诗人阎尔梅，诗文中对微山湖鲤鱼亦是非常喜爱，“烟水昭阳万顷漩，香城隐隐住琴仙；我来闲访红鲤市，偏有邻家认酒钱。”四孔鲤鱼的特点是头小背宽，乌红发亮，身长而健，只有尾巴是红色，而且是有四个鼻孔、四个鱼须，这种外形是微山湖鲤鱼所特有的。鱼的肉质非常鲜嫩，为微山湖区宴席中上品佳肴。之所以是地方特产，就是说换个地方，离开这湖水就没这特点了。所以鲤鱼虽到处都有，但此鲤鱼非彼鲤鱼，你只有在湖区才能品到这鲤鱼。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;济宁家宴&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-wine-glass.jpg&quot; alt=&quot;&quot; /&gt;
图为酒爵, 据说是古代喝酒的容器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-family-banquet.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/jining-change-face.mp4&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;店内的变脸表演&lt;/p&gt;
&lt;h4&gt;民谣酒馆&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-tavern.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-tavern2.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-tavern3.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-cheers.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为 &amp;lt;a href=&quot;https://surl.amap.com/ji10ZYdgfZN&quot; target=&quot;_blank&quot;&amp;gt;吴陆柒民谣酒馆&amp;lt;/a&amp;gt;, 位于秀水河, 周围都是酒馆&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/jining-singing-and-playing.mp4&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;每晚九点有驻场歌手弹唱表演&lt;/p&gt;
&lt;h3&gt;玩乐&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-billiards.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;宿舍附近的台球厅, 距离宿舍非常近, 所以经常光顾&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-ktv.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;压抑的环境需要释放一些多巴胺来缓解&lt;/p&gt;
&lt;h3&gt;探索&lt;/h3&gt;
&lt;h4&gt;济宁市博物馆&lt;/h4&gt;
&lt;p&gt;博物馆人很少, 现场扫码预约即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-museum-center.jpg&quot; alt=&quot;&quot; /&gt;
图为博物馆中心位置, 楼内的廊道螺旋向上, 可以从一楼一直走到五楼&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-museum-window.jpg&quot; alt=&quot;&quot; /&gt;
图为楼内视角&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-kongzi-stone.jpg&quot; alt=&quot;&quot; /&gt;
图为孔子和他的学生&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-guangshan-temple-pagoda.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-museum-monk.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-wukong.jpg&quot; alt=&quot;&quot; /&gt;
图为悟空&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-museum-four.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-museum-window2.jpg&quot; alt=&quot;&quot; /&gt;
窗外为太白湖(市民公园部分)&lt;/p&gt;
&lt;h3&gt;济宁市美术馆&lt;/h3&gt;
&lt;p&gt;人流量很少, 无需预约&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art-gallery.jpg&quot; alt=&quot;&quot; /&gt;
图为济宁市美术馆入口处, 馆内有非常多济宁学院美术学院的学生作品&lt;/p&gt;
&lt;h4&gt;星期一的晚上&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art1.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;续章&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;猫&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art3.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;花&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art4.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;婚礼&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art5.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;剧场&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art6.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;xxx&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art7.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;共生系列插画设计&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-art8.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;孔庙&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple1.jpg&quot; alt=&quot;&quot; /&gt;
图为孔庙入口处的城门 万仞宫墙, 可以看到里面一排的牌坊&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple2.jpg&quot; alt=&quot;&quot; /&gt;
图为棂星门, 皇帝祭天时, 要先祭棂星, 在天文学上, 棂星属于恒星;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple4.jpg&quot; alt=&quot;&quot; /&gt;
图为勾心斗角, 十三碑亭院内, 大成门房檐的一端插入了另一座房屋的两层屋檐的中心位置, 这就是所谓的“钩心”。屋檐的左右四角相对，即所谓的“斗角”。
按孔庙的建筑发展，清代碑亭应建在大中门前，清皇帝为了显示自己，将碑拥挤在各碑亭之前，故在此院内出现了双檐穿插交错的拥挤现象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple5.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/jining-dachengdian.mp4&quot;&amp;gt;&amp;lt;/video&amp;gt;
图为 &lt;strong&gt;大成殿&lt;/strong&gt;, 大成殿为孔庙中的正殿，亦为孔庙中的核心建筑单体，唐代时称为文宣王殿，宋崇宁三年（1104）宋徽宗以《孟子.万章下》“孔子之谓集大成”之义，下诏将曲阜孔庙正殿更名为大成殿，宋政和四年（1114）又颁诏谕定天下孔庙正殿均更名大成殿，元代称宣圣殿，明代称先师庙，清代复称大成殿，意为“集古圣先贤之大成”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple6.jpg&quot; alt=&quot;&quot; /&gt;
图为&lt;strong&gt;龙柱&lt;/strong&gt;, 曲阜孔庙大成殿的四周回廊有２８根高约６米的浮雕龙石柱环绕，石柱全为整石雕刻，共１２９６条神态各异团龙。前檐十根龙柱最精美。滚龙柱采用徽州深浮雕刻工艺，由徽州巧匠制成，10根龙柱各具变化，无一雷同。她是孔庙最为神奇的珍宝，其艺术价值之高，超过故宫金銮殿的贴金龙柱。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple7.jpg&quot; alt=&quot;&quot; /&gt;
图为龙柏树&lt;/p&gt;
&lt;h3&gt;孔府&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple8.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-confucian-temple9.jpg&quot; alt=&quot;&quot; /&gt;
图为重光门&lt;/p&gt;
&lt;h4&gt;南孔&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-confucian-temple3.jpg&quot; alt=&quot;&quot; /&gt;
图为全国部分孔庙书院等儒家文化遗产分布图, 其实历经千年, 基本上历朝历代都将孔子奉为圣人, 孔家地位也非常崇高&lt;/p&gt;
&lt;p&gt;但并不是所有的孔家后裔都在曲阜, 最为著名的是北宋末年南迁的孔子嫡长孙孔端友分支, 称为 &lt;strong&gt;南孔&lt;/strong&gt;, 其弟弟孔端超分支则世代守在曲阜&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;靖康之难后，建炎二年（1128年）十一月，宋高宗赵构于扬州行宫举行继统后首次祀天大典，衍圣公孔端友与堂叔孔传奉诏陪位。孔端友返回曲阜后，&lt;strong&gt;因金兵大举入侵，遂恭负传家宝“孔子及亓官夫人楷木像”（据传为子贡亲手雕刻）、“唐吴道子绘孔子佩剑图”和“至圣文宣王庙祀朱印”等，与部分族人南迁，后家于浙江衢州。其子孙孔玠、孔搢、孔文远、孔万春、孔洙依次承袭衍圣公，史称南宗。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;元朝灭南宋后的至元十九年（1282年），元世祖议立孔子后，以寓衢者为大宗，欲召孔洙回曲阜袭封奉祀。**孔洙称先代庐墓在衢州，不忍离去，乃辞让。**元世祖大加赞赏，称孔洙“宁违荣而不违亲，真圣人后也”。于是下诏免去孔洙的衍圣公爵号，任命其为国子祭酒、承务郎，兼提举浙东学校事，并赠给保护南宗林庙的玺书。同时，为避免南北两宗发生争执，又制定了《整治孔子弟子违犯家规》，既不许违犯圣朝授爵制度，亦不准背忘孔洙德让之风范。&lt;strong&gt;南宗失去爵位后，地位日衰，一度猥如庶氓。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;弘治十八年（1505年），衢州知府沈杰拟〈为条陈孔氏家规以彰圣教〉，上奏明孝宗，“乞将衢州孔端友子孙一人，添授以世袭翰林院五经博士一员，以主家庙祭祀”。明武宗正德元年（1506年），遂 &lt;strong&gt;册封五十九代孙孔彦绳为正八品翰林院五经博士，子孙世袭。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;民国二年（1913年），北洋政府颁布《崇圣典例》，&lt;strong&gt;保留衍圣公爵位，仍由北宗的前清衍圣公孔令贻袭爵，改南宗五经博士孔庆仪为大成至圣先师南宗奉祀官，世袭&lt;/strong&gt;, 1923年冬，孔庆仪去世，其子孔繁豪袭任。国民政府北伐后，孔令贻之子、衍圣公孔德成有感世袭爵位不宜存于民国，遂于1935年主动请求政府撤销爵号。国民政府以为道统不可废，乃改衍圣公作大成至圣先师奉祀官，享特任官的职位及待遇，相当于部长；而孔繁豪仍任大成至圣先师南宗奉祀官，享简任官职位及待遇，约比照司长级，视特任官官阶为低，与孟子“亚圣”、颜子“复圣”、曾子“宗圣”、子思“述圣”奉祀官同等级。1944年10月，孔繁豪去世，无子，乃以其二弟孔繁英长子孔祥楷受封。1949年，&lt;strong&gt;孔祥楷未随国民政府迁台，南宗奉祀官世职遂废。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;徐州&lt;/h2&gt;
&lt;p&gt;来济宁两个月了, 还没有出去玩一玩, 利用周末去了趟徐州&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-stir-fried-chicken.jpg&quot; alt=&quot;&quot; /&gt;
图为地锅鸡, 看起来很小的一锅, 实际上也并不大, 应该是这家店的问题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-cat.jpg&quot; alt=&quot;&quot; /&gt;
图为餐馆里的猫&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-plum-soup.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-plum-soup2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为回龙窝的刘姥姥酸梅汤&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-bread.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为洛馍卷馓子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-metro.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;徐州地铁, 居然有马车标志&lt;/p&gt;
&lt;h3&gt;云龙山&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mountain.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mountain2.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mountain3.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mountain4.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mountain5.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为观景台, 可以一览云龙湖和城区的景色&lt;/p&gt;
&lt;h3&gt;徐州市博物馆&lt;/h3&gt;
&lt;h4&gt;预约购票&lt;/h4&gt;
&lt;p&gt;进入博物馆游览有两种方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;免费预约: 根据微信公众号的 官方公告, &lt;strong&gt;提前一周的 &lt;code&gt;20:00&lt;/code&gt; 放票&lt;/strong&gt;, &lt;strong&gt;其次是当天动态放票&lt;/strong&gt;, 但如果你翻翻小红书就会发现, &amp;lt;span style=&quot;color: red;&quot;&amp;gt;每天 &lt;code&gt;8:55&lt;/code&gt; 到 &lt;code&gt;9:00&lt;/code&gt; 会放出大约一千张票, 这个大量放票的时间点官方只字未提&amp;lt;/span&amp;gt;&lt;/li&gt;
&lt;li&gt;付费购买特展门票(45 💰): &amp;lt;span style=&quot;color: red;&quot;&amp;gt;只要花钱就能进!!!&amp;lt;/span&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种预约规则, 你品, 你细品&lt;/p&gt;
&lt;p&gt;对于没有提前一周 &lt;code&gt;20:00&lt;/code&gt; 准时抢票的游客来说, 基本上 &lt;strong&gt;每次打开预约页面都显示已约满, 这是非常糟糕的预约体验!&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;预约页面&lt;/h4&gt;
&lt;p&gt;预约页面同样设计的令人反感, 好像故意阻碍我预约:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;必须先进入预约页面才能填写预约人信息&lt;/strong&gt;, 等你填好预约人信息, 票早就被别人抢到了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每天全天候不定时放票(每次只有几张), 而且每次都是秒无&lt;/strong&gt;, 这只有刷票脚本才能秒约上吧&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem0.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先, 如果你从未预约过, 并且打开预约页面就是全约满的情况, 那么恭喜你, 你不仅预约不上, 你连预约人的个人信息也没法填写, &lt;strong&gt;在全约满的情况下, 没办法提前填写预约人信息!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem.jpg&quot; alt=&quot;&quot; /&gt;
解决方案就是, 找到有余票的时间段, 大概率这不是你想要游览的时间段, 点击立即预约, 并填写个人信息, 并且不能提交预约&lt;/p&gt;
&lt;p&gt;这个页面还有另外一个坑, 就是你必须点击日期和时间段, 才能点立即预约, 然后进入真正的预约页面:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你要预约的时间恰好有余票, 你需要:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击时间&lt;/li&gt;
&lt;li&gt;点击时间段&lt;/li&gt;
&lt;li&gt;点击立即预约&lt;/li&gt;
&lt;li&gt;选择预约人(没有填写过需要先添加)&lt;/li&gt;
&lt;li&gt;输入手机号(这里没有对手机号进行验证码验证, 也就是说你可以填写任意手机号)&lt;/li&gt;
&lt;li&gt;点击 提交预约&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果这些你都做完了, 你也抢不到票, 因为这时候仅有的几张余票已经没了&lt;/p&gt;
&lt;h4&gt;预约攻略&lt;/h4&gt;
&lt;p&gt;首先你要等到你预约的时间段有余票, 然后点击进入预约页面&lt;/p&gt;
&lt;p&gt;然后正常选择预约人信息, 点击提交预约的时候会报错: &lt;em&gt;当前时间余票不足, 请重新选择&lt;/em&gt;, 这时 &lt;strong&gt;不要离开这个页面, 要继续不停地点击提交预约按钮&lt;/strong&gt;, 因为每点击一次都会发起一个请求, 等到下次有余票了, 恭喜你, 你会以秒级的预约时间击败所有人, 成功完成预约&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem3.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你连续点击了 &lt;code&gt;30&lt;/code&gt; 次 &lt;code&gt;提交预约&lt;/code&gt;, 那么恭喜你, 触发了彩蛋, 这时直接显示了 &lt;code&gt;vConsole&lt;/code&gt;, 通过查看 &lt;code&gt;Network&lt;/code&gt;, 我们发现这跟我预想的一样, 只要请求的频率够快, 就大概率可以成功预约上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem4.jpeg&quot; alt=&quot;&quot; /&gt;
图为接口返回的 &lt;code&gt;json&lt;/code&gt; 数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem5.jpeg&quot; alt=&quot;&quot; /&gt;
图为预约成功页面&lt;/p&gt;
&lt;h4&gt;展品&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits1.jpeg&quot; alt=&quot;&quot; /&gt;
在一楼展厅门口就有自助寄存的提示牌&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits2.jpeg&quot; alt=&quot;&quot; /&gt;
可以在一楼自助寄存行李&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits3.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits4.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits5.jpeg&quot; alt=&quot;&quot; /&gt;
看起来像是存钱罐?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits6.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits7.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits8.jpeg&quot; alt=&quot;&quot; /&gt;
这枕头感觉好硬, 枕着不会头疼吗&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits9.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits10.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits11.jpeg&quot; alt=&quot;&quot; /&gt;
图为金缕玉衣&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits12.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits16.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits13.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits14.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/jining-xuzhou-mesuem-exhibits15.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tjj.jining.gov.cn/module/download/downfile.jsp?classid=0&amp;amp;showname=%E4%BA%8C%E3%80%87%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%BA%A6%E6%B5%8E%E5%AE%81%E5%B8%82%E7%BB%9F%E8%AE%A1%E5%B9%B4%E9%89%B4.pdf&amp;amp;filename=a9d32f56e9c5480f98397534e7560b62.pdf&quot;&gt;二〇二三年度济宁市统计年鉴.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tjj.jining.gov.cn/module/download/downfile.jsp?classid=0&amp;amp;showname=2024%E5%B9%B4%E6%B5%8E%E5%AE%81%E5%B8%82%E5%9B%BD%E6%B0%91%E7%BB%8F%E6%B5%8E%E5%92%8C%E7%A4%BE%E4%BC%9A%E5%8F%91%E5%B1%95%E7%BB%9F%E8%AE%A1%E5%85%AC%E6%8A%A5.pdf&amp;amp;filename=d661b4bfe7454441a5635848f526eeb0.pdf&quot;&gt;2024 年济宁市国民经济和社会发展统计公报.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/351200673/answer/911811350&quot;&gt;为什么济宁不把兖州曲阜邹城连成片?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1q8411m7hs&quot;&gt;济宁城市规划有多难做：位置偏人文差，到处是塌陷区&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://baike.baidu.com/item/%E7%94%8F%E8%82%89%E5%B9%B2%E9%A5%AD/987180&quot;&gt;甏肉干饭&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.weishan.gov.cn/art/2019/1/9/art_23092_1305943.html&quot;&gt;微山湖四鼻孔鲤鱼的传说&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%AD%94%E6%B0%8F%E5%8D%97%E5%AE%97&quot;&gt;孔氏南宗 - Wekipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>2025 扬州南京之行(多图)</title><link>http://blog.xiaban.run/posts/2025/2025-yangzhou-nanjing/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/2025-yangzhou-nanjing/</guid><description>2025 年 5 月份, 我迎来了职业生涯最长的空窗期, 为了防止长时间在家导致的焦虑, 我计划了扬州和南京的短期旅行</description><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;图为南京秦淮河畔平江桥夜景, 拍摄地为旁边的东元桥, &amp;lt;a href=&quot;https://surl.amap.com/1WXisGkd6qC&quot; target=&quot;_blank&quot;&amp;gt;点击查看拍摄地&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;2025&lt;/code&gt; 年 &lt;code&gt;5&lt;/code&gt; 月份, 我迎来了职业生涯最长的空窗期; 乐观来看, 这是难得的放松休息的机会, 但人是一种奇怪的动物, &lt;strong&gt;当生活没有被日程填满时, 就会被焦虑反噬&lt;/strong&gt;; 除此之外, 还要面对大环境下的各种不确定性, 对当下的焦虑和对未知的恐惧情绪让我感觉必须出去走走了 😨&lt;/p&gt;
&lt;h2&gt;出游规划&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;5&lt;/code&gt; 月份迎来了夏天, 恰逢五一假期, 回想起经历过的节假日糟糕的出行体验, 我决定五一假期期间我就不出去了 🤔, 待假期结束就出发&lt;/p&gt;
&lt;p&gt;在选择路线时, 我规划了两条路线:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;郑州 ➡️ 西安&lt;/li&gt;
&lt;li&gt;扬州 ➡️ 南京&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两条路线都很不错, 有我想去玩的地方, 例如很久之前我就刷到了 &lt;a href=&quot;https://www.bilibili.com/video/BV1be4y167Hu/?share_source=copy_web&amp;amp;vd_source=d37db75e273dded1851145dc064395e4&quot;&gt;▶️ 只有河南戏剧幻城&lt;/a&gt;, 对于喜欢看舞台剧的人来说简直太有吸引力了, &lt;em&gt;虽然这次没去, 下次一定 😏&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;最终我选择了扬州南京, 因为我更想去南京&lt;/p&gt;
&lt;h2&gt;行程规划&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-plan-shotcut.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;出发前做了各种规划, 基本按计划都完成了, 但应为天气原因和预约的原因有一部分没有完成, 后文细说&lt;/p&gt;
&lt;p&gt;具体规划如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;扬州
&lt;ul&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/4Gh5sYDBcoK&quot;&amp;gt;中国大运河博物馆&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/ucEumud1u2Dt&quot;&amp;gt;江泽民故居&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/tWy9jq91beCt&quot;&amp;gt;瘦西湖景区&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/uorntoVSb0G&quot;&amp;gt;东关街&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;南京
&lt;ul&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/9PUJVhUe4s&quot;&amp;gt;侵华日军南京大屠杀遇难同胞纪念馆&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/6NGE1cY0L1EH&quot;&amp;gt;秦淮河&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/1blzNSJkgqi&quot;&amp;gt;南京博物院&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/eeqQTI192OH&quot;&amp;gt;中山陵&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/D3gSOHK4OB&quot;&amp;gt;灵谷景区&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;扬州&lt;/h2&gt;
&lt;p&gt;在去扬州之前, 能想到的关于扬州的东西可能就只有江南园林和扬州炒饭 🍚 了, 但在历史上, 扬州是古九州之一, 因其重要的地理位置和优越的自然环境, 在历朝历代几乎都是繁华的工商业城市&lt;/p&gt;
&lt;h3&gt;大运河&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-canal.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-canal2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在古代漕运是非常重要的物资运输方式, 漕粮更是国家战略物资&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-ports.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-ports2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;扬州地处大运河的重要节点, 图中可以看到重要的港口城市如今都已衰落, 主要原因如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;黄河改道导致运河山东段逐渐淤废&lt;/li&gt;
&lt;li&gt;现在海运和铁路运输非常发达&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-dragon-robe.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-fragment.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;江南丝织业发达, 图为丝锦龙袍和龙泉窑碎片&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-stone-man.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;镇水文化历史悠久, 图为三神石人, 为开凿都江堰的李冰, 被古人奉为水神&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-loft.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大运河博物馆楼顶, 视野开阔, 可以看到整个扬州市区, 右侧的阁楼需付费购票进入, 以下是楼顶的视角&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-bridge.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大运河博物馆旁边就是古运河, 近处的桥为剪影桥&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-river.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;古运河中的游船&lt;/p&gt;
&lt;h3&gt;瘦西湖&lt;/h3&gt;
&lt;p&gt;瘦西湖是扬州著名景点, 随处都有江南水乡的意境, 只是门票略贵, 白天 💰 100, 晚上更贵&lt;/p&gt;
&lt;p&gt;瘦西湖名称的来历，是乾隆年间寓居扬州的诗人汪沆的一首感慨富商挥金如土的诗作:“ &lt;strong&gt;垂柳不断接残芜，雁齿红桥俨画图；也是销金一锅子，故应唤作瘦西湖。&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-bridge24.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为二十四桥, 唐代诗人杜牧有流传千古代诗篇《寄扬州韩绰判官》：“&lt;strong&gt;青山隐隐水迢迢，秋尽江南草未凋。二十四桥明月夜，玉人何处教吹箫？&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-lion.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-lion1.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;徐园门外深情对望的狮子 🦁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-duck.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;极速前进的小鸭子&lt;/p&gt;
&lt;h3&gt;江泽民故居&lt;/h3&gt;
&lt;p&gt;由于江老的名字是敏感字, 所以在小红书上搜索不到任何帖子, 在抖音上也只能搜到一模一样的重复的文案, 很少有介绍如何预约的文章&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-jiang-search.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等我找到预约方式时, 就只能预约下周的了, 所以很遗憾 😭 没有去成&lt;/p&gt;
&lt;p&gt;预约方式为: 微信小程序直接搜索以上关键字, 进入小程序, 并且至少提前一周预约, 根据预约须知, &lt;strong&gt;内部不能拍照&lt;/strong&gt;, 地址在 &amp;lt;a target=&quot;_blank&quot; href=&quot;https://surl.amap.com/ucEumud1u2Dt&quot;&amp;gt;东关街附近&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;南京&lt;/h2&gt;
&lt;p&gt;都说 &lt;strong&gt;一座南京城, 半部民国史&lt;/strong&gt;, 也有 &lt;strong&gt;南京是亡国之都&lt;/strong&gt; 的说法, 南京城也是六朝古都, 十朝都会, 但近代民国时期的国力孱弱和战争历程, 也使得我们每每提到南京时, 都会想到它的波折跌宕的历史&lt;/p&gt;
&lt;h3&gt;梧桐树&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-sycamore.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-tree.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;南京城区的梧桐树, 南京真的路边都是梧桐树, 当然最好看的还是 &lt;a href=&quot;https://www.xiaohongshu.com/discovery/item/66f95b7c000000002c02b175?source=webshare&amp;amp;xhsshare=pc_web&amp;amp;xsec_token=AB00WqJT3cPVryPYMkcepvokr8l_4NM85PI2XvweozYqE=&amp;amp;xsec_source=pc_share&quot;&gt;🔗 梧桐大道&lt;/a&gt;, 位于 &lt;a href=&quot;https://surl.amap.com/cd0W8OoL4rK&quot;&gt;钟山风景区南京梧桐大道&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;鸭血粉丝汤&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-soup.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-soup2.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-soup3.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-soup4.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;鸭血粉丝汤和烤鸭包, 汤鲜味甜&lt;/p&gt;
&lt;h3&gt;侵华日军南京大屠杀遇难同胞纪念馆&lt;/h3&gt;
&lt;p&gt;在南京这片土地上, 发生过惨绝人寰的南京大屠杀, 这对南京人乃至中华民族来说都是一段痛苦沉痛的记忆, 但我们尊重历史, 也尊重遇难同胞, 所以我们在 1985 年 江东门集体屠杀遗址和万人坑遗址上建起了这座纪念馆, 以告慰遇难同胞, 在展示大屠杀的残酷血腥的同时, 也向全世界展示了我们珍爱和平, 维护和平的决心&lt;/p&gt;
&lt;h4&gt;时间线&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;1937 年 8 月 13 日, 日军进攻上海, 中国军队顽强抵抗了三个月才攻破沿岸地区, 也就是惨烈的 &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%B7%9E%E6%B2%AA%E4%BC%9A%E6%88%98&quot;&gt;淞沪会战&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;1937 年 11 月中旬, 日军决定进攻首都南京&lt;/li&gt;
&lt;li&gt;1937 年 11 月 20 日，蒋介石任命唐生智为南京卫戍司令官, 固守南京&lt;/li&gt;
&lt;li&gt;1937 年 12 月 8 日，日军全面占领南京外围阵地&lt;/li&gt;
&lt;li&gt;1937 年 12 月 13 日, 南京沦陷, 日军开始了惨无人道的大屠杀, 日军犯下了一系列罄竹难书的反人类罪行, 持续 6 周以上&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;雕塑&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-sculpture.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-sculpture2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为雕塑 《家破人亡》, 充满了绝望与无助&lt;/p&gt;
&lt;h4&gt;公祭广场&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-square.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用 11 种文字展示的 &lt;code&gt;遇难者 30 万&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;馆内&lt;/h4&gt;
&lt;p&gt;馆内展品很多, 让人感觉心情特别沉重, 只拍了很少的一部分:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-peoples.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-peoples2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;南京保卫战部分殉国将士名录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-soil.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-remember-history.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;馆外&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-peace.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为和平雕像&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/nanjing-history.mp4&quot; type=&quot;video/quicktime&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;南京下起了大雨, 我在雨中参观了遇难同胞纪念馆&lt;/p&gt;
&lt;h3&gt;南京博物院&lt;/h3&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/nanjing-yangzhou-horse.mp4&quot; type=&quot;video/quicktime&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-horse.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;博物院内的网红🐴 太抽象了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum1.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum2.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum3.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum4.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum5.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum6.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum7.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum8.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-museum9.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;钟山风景区&lt;/h3&gt;
&lt;p&gt;钟山风景区非常大, 包含了: 明孝陵 / 美龄宫 / 中山陵 / 灵谷景区 等, 由于时间关系我只去了 &lt;a href=&quot;#%E4%B8%AD%E5%B1%B1%E9%99%B5&quot;&gt;中山陵&lt;/a&gt; 和 灵谷景区&lt;/p&gt;
&lt;h4&gt;行程&lt;/h4&gt;
&lt;p&gt;🚌 乘坐观光车 1 号线 从 🚇 苜蓿园站 前往 中山陵南站, &amp;lt;a href=&quot;https://surl.amap.com/3bbYa051hcqi&quot; target=&quot;_blank&quot;&amp;gt;点击查看路线&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-zhongshanling-plan1.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;🚌 乘坐观光车 4 号线 到达灵谷景区, &amp;lt;a href=&quot;https://surl.amap.com/2uICGHz1efcx&quot; target=&quot;_blank&quot;&amp;gt;点击查看路线&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-zhongshanling-plan2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 注意: 景区观光车 💰 10 块一次, 包天 💰 30 块, 建议根据导航优先乘坐普通公交车&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;中山陵&lt;/h3&gt;
&lt;p&gt;中山陵是孙中山先生的陵墓, 孙先生逝世后国民政府遵照他的遗愿在南京为其修建陵墓, 中山陵吸取中国古代陵墓的对称布局，利用墓道和台阶将主要建筑连为轴线，并布置大片绿地，将陵墓建筑群联接成与背景山势相称的宏大整体。陵墓主体建筑采用中国古典宫殿式建筑的大屋顶造型，应用西方建筑的设计和建造技术，以肃穆的蓝白色调和严谨的建筑构型表现出沉静恢弘的气度，被认为是中国近现代建筑史上融合中西的经典作品&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-go-up-the-mountain.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-go-up-the-mountain2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为前往中山陵的路上, 没想到避开了节假日和周末, 还是没有避开学生和老年团 😭, 人还是很多&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-go-up-the-mountain-love.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为博爱坊 &lt;a href=&quot;https://surl.amap.com/6EHlcHcc1j5RM&quot;&gt;📌&lt;/a&gt;, 中门横楣上的石额刻有孙中山手书的“博爱”二个鎏金大字&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-mausoleum-gate.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为陵门 &lt;a href=&quot;https://surl.amap.com/z09D1djjd86&quot;&gt;📌&lt;/a&gt;, 陵门是中山陵的正门, 处于中山陵中轴线正中, “天下为公”出自《礼记·礼运》之“大道之行也，天下为公。” &lt;strong&gt;孙中山将“天下为公”借用为对“民权主义”的解释，说明政权为广大平民所共有&lt;/strong&gt;, 这一思想对于近代中国的思想观念的转变具有重要意义&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-stone-tablet.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为碑亭 &lt;a href=&quot;https://surl.amap.com/cx3Tr0p1c4ZH&quot;&gt;📌&lt;/a&gt;, “中国国民党葬 总理孙先生于此 中华民国十八年六月一日”，由谭延闿以颜体书写&lt;/p&gt;
&lt;h4&gt;祭堂&lt;/h4&gt;
&lt;p&gt;祭堂位于中山陵最顶部&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-memorial-hall.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-memorial-hall2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;门楣上方的额枋上从东到西分别为张静江所书写的“民族”、“民权”、“民生”六个篆书金字，代表国父孙中山创立的三民主义的三个组成部分。正中间的民生门略大，上下重檐之间嵌有直额，上刻有孙中山所题“天地正气”四个金字&lt;/p&gt;
&lt;p&gt;祭堂内部需要从右侧排队进入参观&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-memorial-hall3.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在祭堂顶部眺望南京城区&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-memorial-hall4.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在祭堂顶部俯瞰陵门&lt;/p&gt;
&lt;h4&gt;孙中山纪念馆&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-sun-yat-sen-memorial-hall.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-sun-yat-sen-memorial-hall2.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-sun-yat-sen-memorial-hall3.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;灵谷景区&lt;/h3&gt;
&lt;h4&gt;灵谷胜境&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-jinggushengjing.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;阵亡将士公墓牌坊&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-cemetery-gate.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-cemetery-gate2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;无梁殿&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-wuliangdian.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该殿建于洪武十四年（1381年），原为灵谷寺内供奉无量寿佛的无量殿，因为整座建筑采用砖砌拱券结构、不设木梁，因此又称“无梁殿”。民国二十一年（1931年），国民政府将无梁殿改建为国民革命军阵亡将士公墓的祭堂，命名为“正气堂”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-wuliangdian2.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-wuliangdian3.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;淞沪抗战阵亡将士纪念碑&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-monument.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-monument2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为第十九路军淞沪抗战阵亡将士纪念碑, 第十九路军是一支粤军, 淞沪抗战在淞沪会战之前, 第十九路军也是首个与日军全面对抗的中国军队, 以几乎全军覆没为代价抵抗了日军多次进攻&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-monument3.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-monument4.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图为第五军淞沪抗战阵亡将士纪念碑, 1月30日，中国国民政府宣布迁都洛阳，蒋通电抗日，并派精锐之第五军增援上海&lt;/p&gt;
&lt;h3&gt;秦淮河&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/nanjing-yangzhou-qinhuai.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-yangzhou-qinhuai2.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/nanjing-topic.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%BA%AC%E6%9D%AD%E5%A4%A7%E8%BF%90%E6%B2%B3&quot;&gt;京杭大运河 - wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E4%B8%AD%E5%B1%B1%E9%99%B5#%E7%A2%91%E4%BA%AD&quot;&gt;中山陵 - wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%81%B5%E8%B0%B7%E5%AF%BA%E6%97%A0%E6%A2%81%E6%AE%BF&quot;&gt;灵谷寺无梁殿 - wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Cherry Studio MCP 初体验</title><link>http://blog.xiaban.run/posts/2025/cherry-studio-mcp/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/cherry-studio-mcp/</guid><description>在 LLM 被普遍使用的今天, MCP 大大提升了 LLM 的实用性和本地化操作的能力, 本文将介绍如何基于 Cherry Studio 来体验 MCP 的强大能力</description><pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;LLM&lt;/code&gt; 被普遍使用的今天, &lt;code&gt;MCP&lt;/code&gt; 大大提升了 &lt;code&gt;LLM&lt;/code&gt; 的实用性和本地化操作的能力, 本文将介绍如何基于 &lt;a href=&quot;https://cherry-ai.com/&quot;&gt;Cherry Studio&lt;/a&gt; 来体验 &lt;code&gt;MCP&lt;/code&gt; 的强大能力&lt;/p&gt;
&lt;p&gt;其实在选择支持 &lt;code&gt;MCP&lt;/code&gt; 的客户端时, 我更加倾向于使用 &lt;code&gt;vscode&lt;/code&gt;, 因为它使用的是 &lt;code&gt;Github Copilot&lt;/code&gt;, 而且已经 &lt;a href=&quot;https://code.visualstudio.com/blogs/2025/04/07/agentMode&quot;&gt;在最新版对 &lt;code&gt;MCP&lt;/code&gt; 进行了支持&lt;/a&gt;, 但 &lt;code&gt;vscode&lt;/code&gt; 需要付费才能自定义使用的模型, 所以我选择了更加通用的免费的 &lt;a href=&quot;https://cherry-ai.com/&quot;&gt;Cherry Studio&lt;/a&gt;, 当然现阶段还有其他的支持 &lt;code&gt;MCP&lt;/code&gt; 的客户端, 这里我选择了 &lt;a href=&quot;https://cherry-ai.com/&quot;&gt;Cherry Studio&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;Cherry Studio&lt;/h3&gt;
&lt;p&gt;直接从 &lt;a href=&quot;https://cherry-ai.com/download&quot;&gt;download page&lt;/a&gt; 下载&lt;/p&gt;
&lt;p&gt;然后需要创建 &lt;code&gt;~/.cherrystudio/bin&lt;/code&gt; 目录, 用于存放 &lt;code&gt;bun&lt;/code&gt; / &lt;code&gt;uv&lt;/code&gt; / &lt;code&gt;uvx&lt;/code&gt;, 这是 &lt;code&gt;Cherry Studio&lt;/code&gt; &lt;code&gt;MCP&lt;/code&gt; 服务所需的依赖, 可参考 &lt;a href=&quot;https://docs.cherry-ai.com/advanced-basic/mcp/install&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.cherrystudio/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方只使用 &lt;code&gt;~/.cherrystudio/bin&lt;/code&gt; 中的 &lt;code&gt;bun&lt;/code&gt; / &lt;code&gt;uv&lt;/code&gt; / &lt;code&gt;uvx&lt;/code&gt;, 而不是全局的 &lt;code&gt;bun&lt;/code&gt; / &lt;code&gt;uv&lt;/code&gt; / &lt;code&gt;uvx&lt;/code&gt;, 这很令人不解, 这里选择手动全局安装, 然后软连接到 &lt;code&gt;~/.cherrystudio/bin&lt;/code&gt;, 下面将介绍如何安装及配置这些依赖&lt;/p&gt;
&lt;h3&gt;bun&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;我是 &lt;code&gt;MacOS&lt;/code&gt; 和 &lt;code&gt;fish shell&lt;/code&gt;, 以下命令适用于我的环境, 下文不再赘述;
其他系统安装参考官方页面 &lt;a href=&quot;https://bun.sh/docs/installation&quot;&gt;installation - bun&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://bun.sh/install | bash # for macOS, Linux, and WSL
source ~/.config/fish/config.fish # 重新载入环境变量
sudo ln -s $(which bun) ~/.cherrystudio/bin/bun # 将 bun 软链接到 ~/.cherrystudio/bin/bun
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;uv&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;其他系统安装参考官方页面 &lt;a href=&quot;https://docs.astral.sh/uv/getting-started/installation/&quot;&gt;installation - uv&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env.fish # 重新载入环境变量
sudo ln -s $(which uv) ~/.cherrystudio/bin/uv
sudo ln -s $(which uvx) ~/.cherrystudio/bin/uvx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;检查 mcp 安装&lt;/h3&gt;
&lt;p&gt;进入 &lt;em&gt;设置&lt;/em&gt; -&amp;gt; &lt;em&gt;MCP 服务器&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/cherry-studio-mcp-panel.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用 MCP&lt;/h2&gt;
&lt;h3&gt;添加 MCP 服务&lt;/h3&gt;
&lt;p&gt;这里我们以 &lt;a href=&quot;https://mcp.so/server/amap-maps/amap&quot;&gt;高德地图官方的 MCP Server&lt;/a&gt; 为例:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://mcp.so/server/amap-maps/amap&quot;&gt;Amap Maps - mcp.so&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;找到 &lt;strong&gt;服务器配置&lt;/strong&gt;, 按照配置中的 &lt;code&gt;command&lt;/code&gt; / &lt;code&gt;args&lt;/code&gt; / &lt;code&gt;env&lt;/code&gt; 创建 &lt;em&gt;MCP 服务器&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/cherry-studio-mcpso.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/cherry-studio-amap-create.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://console.amap.com/&quot;&gt;高德地图开放平台控制台&lt;/a&gt;, 进入 &lt;em&gt;应用管理&lt;/em&gt; -&amp;gt; &lt;em&gt;创建新应用&lt;/em&gt;, 服务平台选择 &lt;em&gt;Web 平台&lt;/em&gt;, 然后在 &lt;em&gt;我的应用&lt;/em&gt; 中找到 &lt;em&gt;Key&lt;/em&gt;, 并复制到 &lt;code&gt;Cherry Studio&lt;/code&gt; 中新创建的 &lt;em&gt;高德地图&lt;/em&gt; 服务中
&lt;img src=&quot;./assets/images/cherry-studio-amap-apps.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点保存并启用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新建聊天并选择我们刚创建的 &lt;code&gt;Amap Maps&lt;/code&gt; 服务&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/cherry-studio-select-mcp.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/static-videos/cherry-studio-amap-test.mp4&quot; type=&quot;video/mp4&quot;&amp;gt;
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;翻车了朋友们 😓, 天津南站明明有地铁站, 竟然让我步行到下瓦房站; 在查看 高德地图的 &lt;code&gt;tools&lt;/code&gt; 时发现, 大模型没有调用 &lt;code&gt;maps_geo&lt;/code&gt;, 正确的调用方式应该是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 &lt;code&gt;maps_gep&lt;/code&gt;, 获取起点和终点的准确经纬度&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;maps_direction_transit_integrated&lt;/code&gt;, 传入起点和终点经纬度, 获取行程信息&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;maps_around_search&lt;/code&gt; 获取周边酒店信息&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/cherry-studio-amap-tools.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
看来 &lt;code&gt;LLM&lt;/code&gt; 的能力才是最基本的, 一个好的模型才能发挥 &lt;code&gt;MCP&lt;/code&gt; 的能力, 这里我用的是 &lt;code&gt;qwen-turbo&lt;/code&gt;, 属实拉胯&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cherry-ai.com/&quot;&gt;Cherry Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/uv/getting-started/installation/&quot;&gt;installation - uv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/installation&quot;&gt;installation - bun&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 certbot 自动续签 Let&apos;s Encrypt 免费证书</title><link>http://blog.xiaban.run/posts/2025/certbot/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/certbot/</guid><description>现在阿里云的免费证书有效期只有 3 个月, 过期之后还要重新签发 😡, 所以我开始尝试寻找免费可自动续签的证书服务, 就是使用 Certbot 配置 Lets Encrypt 免费证书</description><pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;现在阿里云的免费证书有效期只有 3 个月, 过期之后还要重新签发 😡, 所以我开始尝试寻找免费可自动续签的证书服务, 就是使用 &lt;a href=&quot;https://github.com/certbot/certbot&quot;&gt;Certbot&lt;/a&gt; 配置 &lt;a href=&quot;https://letsencrypt.org/zh-cn/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt; 免费证书&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;h3&gt;Let&apos;s Encrypt&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://letsencrypt.org/zh-cn/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt;: 这是一个由非营利性组织互联网安全研究小组（&lt;code&gt;ISRG&lt;/code&gt;）提供的免费、自动化和开放的证书颁发机构。它为众多网站提供 &lt;code&gt;TLS&lt;/code&gt; 证书，其免费证书的签发/续签可以通过脚本自动化完成, &lt;strong&gt;简而言之就是可以免费一直用&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;certbot&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://letsencrypt.org/zh-cn/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt; 使用 &lt;code&gt;ACME&lt;/code&gt; 协议来验证您对给定域名的控制权并向您颁发证书。 要获得 &lt;a href=&quot;https://letsencrypt.org/zh-cn/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt; 证书，您需要选择一个使用 ACME 客户端软件, 而 &lt;a href=&quot;https://github.com/certbot/certbot&quot;&gt;Certbot&lt;/a&gt; 就是 &lt;a href=&quot;https://letsencrypt.org/zh-cn/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt; 推荐的客户端&lt;/p&gt;
&lt;h2&gt;安装 cerbot&lt;/h2&gt;
&lt;p&gt;:::tip
服务器环境为 &lt;code&gt;Ubuntu 22.04&lt;/code&gt;
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo snap install --classic certbot
# sudo apt-get install certbot # 或者使用 apt 安装
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 aliyun cli&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;wget https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz
tar xzvf aliyun-cli-linux-latest-amd64.tgz
sudo cp aliyun /usr/local/bin
rm aliyun
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 aliyun cli&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;RAM&lt;/code&gt; 用户, 参考 &lt;a href=&quot;https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair?spm=a2c4g.11186623.0.0.50e633afOaV9gV#title-ebf-nrl-l0i&quot;&gt;创建 RAM 用户的 AccessKey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;em&gt;权限管理&lt;/em&gt; -&amp;gt; &lt;em&gt;新增授权&lt;/em&gt;, 为 &lt;code&gt;RAM&lt;/code&gt; 用户添加 &lt;code&gt;DNS&lt;/code&gt; 解析相关的权限(可在权限列表中搜索 &lt;code&gt;DNS&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/certbot-aliyun-ram-user-roles.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;点击 &lt;em&gt;认证管理&lt;/em&gt; -&amp;gt; &lt;em&gt;AccessKey&lt;/em&gt;, 创建 &lt;code&gt;AccessKey&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 &lt;code&gt;AccessKey&lt;/code&gt; 的网络访问权限
&lt;img src=&quot;./assets/images/certbot-aliyun-ran-accesskey.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 aliyun configure&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;aliyun configure
Configuring profile &apos;default&apos; in &apos;AK&apos; authenticate mode...
Access Key Id [*********************SQK]:
Access Key Secret [***************************LJL]:
Default Region Id [cn-hangzhou]:
Default Output Format [json]: json (Only support json)
Default Language [zh|en] zh:
Saving profile[default] ...Done.

Configure Done!!!
..............888888888888888888888 ........=8888888888888888888D=..............
...........88888888888888888888888 ..........D8888888888888888888888I...........
.........,8888888888888ZI: ...........................=Z88D8888888888D..........
.........+88888888 ..........................................88888888D..........
.........+88888888 .......Welcome to use Alibaba Cloud.......O8888888D..........
.........+88888888 ............. ************* ..............O8888888D..........
.........+88888888 .... Command Line Interface(Reloaded) ....O8888888D..........
.........+88888888...........................................88888888D..........
..........D888888888888DO+. ..........................?ND888888888888D..........
...........O8888888888888888888888...........D8888888888888888888888=...........
............ .:D8888888888888888888.........78888888888888888888O ..............
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
输出最后的阿里云 &lt;code&gt;logo&lt;/code&gt; 图案表示配置成功&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装 certbot-dns-aliyun 插件&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;justjavac/certbot-dns-aliyun&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://cdn.jsdelivr.net/gh/justjavac/certbot-dns-aliyun@main/alidns.sh
sudo cp alidns.sh /usr/local/bin
sudo chmod +x /usr/local/bin/alidns.sh
sudo ln -s /usr/local/bin/alidns.sh /usr/local/bin/alidns
rm alidns.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;申请证书&lt;/h2&gt;
&lt;h3&gt;测试是否能正确申请证书&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;certbot certonly -d &quot;*.example.com&quot; --manual --preferred-challenges dns --manual-auth-hook &quot;alidns&quot; --manual-cleanup-hook &quot;alidns clean&quot; --dry-run

The following error was encountered:
[Errno 13] Permission denied: &apos;/var/log/letsencrypt/.certbot.lock&apos;
Either run as root, or set --config-dir, --work-dir, and --logs-dir to writeable paths.
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /tmp/certbot-log-4krk5x0b/log or re-run Certbot with -v for more details.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
把 &lt;code&gt;example.com&lt;/code&gt; 替换为实际的域名&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里我们使用的是非 &lt;code&gt;root&lt;/code&gt; 用户, 所以是没有 &lt;code&gt;/var/log/letsencrypt&lt;/code&gt; 的访问权限的, 所以我们根据提示信息配置生成的目录:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certbot certonly -d &quot;*.example.com&quot; --config-dir $HOME/.certbot/config --work-dir $HOME/.certbot/work --logs-dir $HOME/.certbot/logs --manual --preferred-challenges dns --manual-auth-hook &quot;alidns&quot; --manual-cleanup-hook &quot;alidns clean&quot; --dry-run
Saving debug log to /home/admin/.certbot/logs/letsencrypt.log
Enter email address or hit Enter to skip.
 (Enter &apos;c&apos; to cancel):

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at:
https://letsencrypt.org/documents/LE-SA-v1.5-February-24-2025.pdf
You must agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
Account registered.
Simulating a certificate request for *.example.com
Hook &apos;--manual-auth-hook&apos; for example.com ran with output:
 {
 	&quot;RecordId&quot;: &quot;1234&quot;,
 	&quot;RequestId&quot;: &quot;5678&quot;
 }
Hook &apos;--manual-cleanup-hook&apos; for example.com ran with output:
 {
 	&quot;RecordId&quot;: &quot;1234&quot;,
 	&quot;RequestId&quot;: &quot;5678&quot;
 }
The dry run was successful.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;~/.certbot/config&lt;/code&gt; 是存放证书和私钥的目录, 请按照实际情况进行修改&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
此命令申请的是 &lt;code&gt;*.example.com&lt;/code&gt; 的证书, 如需同时申请顶级域名或其他 &lt;strong&gt;多个域名的证书&lt;/strong&gt;, 可以增加 &lt;code&gt;-d example.com&lt;/code&gt; 参数生成, 并增加 &lt;code&gt;--cert-name&lt;/code&gt; 指定生成的目录(因为生成顶级域名和子域域名证书时, 生成的目录名可能重复, 此时会生成例如 &lt;code&gt;example.com-0001&lt;/code&gt; 的目录名)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;正式申请证书&lt;/h3&gt;
&lt;p&gt;把 &lt;code&gt;--dry-run&lt;/code&gt; 参数去掉就可以申请到证书了, 只要有 &lt;code&gt;--dry-run&lt;/code&gt; 参数时执行成功, 就可以正确生成证书了&lt;/p&gt;
&lt;p&gt;最终的命令如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 申请顶级域名的证书
certbot certonly -d &quot;example.com&quot; --cert-name &quot;example.com&quot; --config-dir $HOME/.certbot/config --work-dir $HOME/.certbot/work --logs-dir $HOME/.certbot/logs --manual --preferred-challenges dns --manual-auth-hook &quot;alidns&quot; --manual-cleanup-hook &quot;alidns clean&quot;

# 2. 申请一级子域名通配符证书
certbot certonly -d &quot;*.example.com&quot; --cert-name &quot;all.example.com&quot; --config-dir $HOME/.certbot/config --work-dir $HOME/.certbot/work --logs-dir $HOME/.certbot/logs --manual --preferred-challenges dns --manual-auth-hook &quot;alidns&quot; --manual-cleanup-hook &quot;alidns clean&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;证书续期&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;certbot renew --manual --config-dir $HOME/.certbot/config --work-dir $HOME/.certbot/work --logs-dir $HOME/.certbot/logs --preferred-challenges dns --manual-auth-hook &quot;alidns&quot; --manual-cleanup-hook &quot;alidns clean&quot;
Saving debug log to /home/admin/.certbot/logs/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /home/admin/.certbot/config/renewal/all.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /home/admin/.certbot/config/renewal/example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificates are not due for renewal yet:
  /home/admin/.certbot/config/live/all.example.com/fullchain.pem expires on 2025-07-26 (skipped)
  /home/admin/.certbot/config/live/example.com/fullchain.pem expires on 2025-07-26 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置定时自动续期&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# certbot renew
1 1 */1 * * bash -l -c &apos;source /home/admin/.profile &amp;amp;&amp;amp; certbot renew --manual --config-dir $HOME/.certbot/config --work-dir $HOME/.certbot/work --logs-dir $HOME/.certbot/logs --preferred-challenges dns --manual-auth-hook &quot;alidns&quot; --manual-cleanup-hook &quot;alidns clean&quot; --deploy-hook &quot;sudo nginx -s reload&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我用的是 &lt;code&gt;admin&lt;/code&gt; 用户, 而不是 &lt;code&gt;root&lt;/code&gt;, 所以 &lt;code&gt;source /home/admin/.profile&lt;/code&gt; 是为了加载 &lt;code&gt;admin&lt;/code&gt; 用户的环境变量&lt;/p&gt;
&lt;h3&gt;配置 nginx&lt;/h3&gt;
&lt;p&gt;配置一下 &lt;code&gt;nginx&lt;/code&gt; 配置文件, 创建一个软连接将指定域名的证书文件链接到我们生成的证书目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
  listen 443 ssl;
  server_name example.com;
  charset utf-8;

  ###### certbot START ########
  #填写证书文件绝对路径
  ssl_certificate conf.d/certbot/example.com/fullchain.pem;
  #填写证书私钥文件绝对路径
  ssl_certificate_key conf.d/certbot/example.com/privkey.pem;
  ###### certbot END ########

	ssl_session_cache shared:SSL:1m;
	ssl_session_timeout 5m;

	#自定义设置使用的TLS协议的类型以及加密套件（以下为配置示例，请您自行评估是否需要配置）
	#TLS协议版本越高，HTTPS通信的安全性越高，但是相较于低版本TLS协议，高版本TLS协议对浏览器的兼容性较差。
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
	ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;

	#表示优先使用服务端加密套件。默认开启
	ssl_prefer_server_ciphers on;

  # example.com =&amp;gt; blog.example.com
  rewrite ^/(.*)$ https://blog.example.com/$1 permanent;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;ssl_certificate&lt;/code&gt; 是证书文件路径, &lt;code&gt;ssl_certificate_key&lt;/code&gt; 是证书私钥文件路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ln -s /home/admin/.certbot/config/live/example.com /etc/nginx/conf.d/certbot/example.com
sudo ln -s /home/admin/.certbot/config/live/all.example.com /etc/nginx/conf.d/certbot/all.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
这里根据实际情况进行配置, 以上路径只是实例&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后在浏览器中查看证书信息:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/certbot-browser-view.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果颁发者组织是 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt;, 表示配置成功 🎉&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/certbot/certbot&quot;&gt;Certbot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://letsencrypt.org/zh-cn/&quot;&gt;Let&apos;s Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://letsencrypt.org/zh-cn/docs/client-options/&quot;&gt;ACME 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7383263356184641573&quot;&gt;使用Let&apos;s Encrypt 申请通配符证书&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/justjavac/certbot-dns-aliyun&quot;&gt;certbot-dns-aliyun&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>2025 天津之行(多图)</title><link>http://blog.xiaban.run/posts/2025/2025-tianjin/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/2025-tianjin/</guid><description>2025 年春末, 我利用周末的时间, 去了一趟天津</description><pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;2025&lt;/code&gt; 年的我已经无限接近 &lt;code&gt;30&lt;/code&gt; 岁, 回顾最近几年, 好像一直忙于工作, 传统观念中的三十而立已至, 然而我的生活却毫无变化;&lt;/p&gt;
&lt;p&gt;最近看到一句话, &lt;strong&gt;人无法判断一个瞬间的价值, 直到它成为回忆&lt;/strong&gt;; 我无法确定这个瞬间是指什么, 但肯定不是指日复一日的敲代码, 也不是指每天上下班的路途, 或许我的生活 &lt;strong&gt;缺少值得回忆的瞬间&lt;/strong&gt;, 我有点理解人为什么都期待旅行, 因为旅行可以使自己跳出物理生活圈, 感受旅途中的每一件事物, 或许这就是瞬间的价值;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果人生是一场开放世界游戏, 那么旅行就是一个个支线任务, 它是感受游戏世界的绝佳机会&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;行程规划&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;酒店: 考虑到要去滨海区, 所以选择了 9 号线 附近, 并且最好是中转站, 所以选择了 &lt;em&gt;直沽&lt;/em&gt; 附近, 附近的酒店是真贵啊, 普通的周末最便宜的房间三百多 😭&lt;/li&gt;
&lt;li&gt;行程:
&lt;ul&gt;
&lt;li&gt;天津站附近&lt;/li&gt;
&lt;li&gt;五大道&lt;/li&gt;
&lt;li&gt;海港公园&lt;/li&gt;
&lt;li&gt;天津博物馆&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;备忘录中的攻略截图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-travel-guide.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实际走下来, 最值得去的是天津博物馆和意风区&lt;/p&gt;
&lt;h2&gt;启程&lt;/h2&gt;
&lt;p&gt;在周五晚从我所在的城市坐 🚄 出发, 可以到达天津, 虽然要中转, 但 3h 就能到, 所以其实天津是很近的&lt;/p&gt;
&lt;p&gt;由于高铁晚点 &lt;code&gt;14 min&lt;/code&gt;, 所以直到 &lt;code&gt;22:50&lt;/code&gt; 才到 天津南站 🚉, 原本计划坐 &lt;code&gt;1h&lt;/code&gt; 地铁, 但时间太晚了, 坐到营口道站就打车去了酒店, 到达酒店就已经 &lt;code&gt;23:50&lt;/code&gt; 了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-hotel-door.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;刚进门就发现门框快掉下来了, 真是毫不避讳, 然而我已经困得不行了 😴, 马上就休息了一下睡觉了&lt;/p&gt;
&lt;h2&gt;天津站附近&lt;/h2&gt;
&lt;h3&gt;津湾广场&lt;/h3&gt;
&lt;p&gt;坐地铁到达 🚇 津湾广场站, 对岸就是天津站, 解放桥和世纪钟就是城市地标 😎&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-century-clock.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解放桥 / 海河 / 世纪钟&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-liberation-bridge.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;早期解放桥&lt;/p&gt;
&lt;p&gt;这座风格独特、现代感极强的铁桥，出自法国建筑设计大师，巴黎艾菲尔铁塔的设计者居斯塔夫·埃菲尔之手&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-dagu-bridge.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大沽桥下, 大爷们正在悠闲地钓鱼🎣&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-light-bridge.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大光明桥夜景&lt;/p&gt;
&lt;p&gt;天津是沿海河发展的, 所以桥特别多, 而且建造的非常精致, 当年这里是 9 国租界, 如今这里是我国直辖市, 北方第二城&lt;/p&gt;
&lt;h3&gt;意式风情区&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-Italian-style-area-crossing.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;意式风情区 🇮🇹 的一个路口, 商业化非常足, 路口处有天津工业展览馆, 电动三轮车为游客提供讲解服务, 这里可以坐三轮车的, 但我更喜欢步行 🚶&lt;/p&gt;
&lt;p&gt;这里的商业化比较严重, 虽然有很多独特的建筑, 但我对建筑不是很感兴趣, 再放几张有意思的照片:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-idalian-barracks.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;意大利兵营, 现在已经是纯粹的商业区了, 里面有各种餐饮店, 还有画室, 感觉商业化比较严重, 就没进去&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianijn-idalian-statue.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;充满意大利风情的雕像, 不明白为啥这么多人围着看&lt;/p&gt;
&lt;h3&gt;游泳大爷&lt;/h3&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/tianjin-swimming.MOV&quot; type=&quot;video/quicktime&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;点击播放 ▶️, 据说冬天也会来游泳 🏊🏻&lt;/p&gt;
&lt;h2&gt;海港公园&lt;/h2&gt;
&lt;p&gt;海港公园是十年前大爆炸的核心区域, 互联网上几乎没有关于这个公园的介绍, 我为什么要来这里呢? 因为前段时间刷到了这个视频:&lt;/p&gt;
&lt;p&gt;&amp;lt;video controls width=&quot;100%&quot; src=&quot;/static-videos/tianjin-firefighter.mp4&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;这里不再描写当年 &lt;a href=&quot;https://baike.baidu.com/item/8%C2%B712%E5%A4%A9%E6%B4%A5%E6%BB%A8%E6%B5%B7%E6%96%B0%E5%8C%BA%E7%88%86%E7%82%B8%E4%BA%8B%E6%95%85/18370029&quot;&gt;大爆炸事故&lt;/a&gt; 的惨烈, 只放一张网图来对比过去和现在:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-port-diff.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2015&lt;/code&gt; 年我还在上大学, 依稀记得 &lt;code&gt;QQ&lt;/code&gt; 群里曾经传过一些爆炸的视频, 问了几个人都说不记得这件事了, 果然 &lt;strong&gt;时间是一剂良药&lt;/strong&gt;, 互联网也是没有记忆的&lt;/p&gt;
&lt;p&gt;现在的公园:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-harbour-park1.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;公园前广场, 海港公园&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-harbour-park2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;海港公园介绍, 没有任何关于大爆炸的记录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-harbour-park3.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;漫步在蜿蜒的小路 🚶, 非常解压, 路旁的草地中长满了黄色的小花朵&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-harbour-park4.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-harbour-park5.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-harbour-park6.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;令人不解的是, 当年爆炸留下的建筑依然没有没拆掉, 不过后面的小区建筑非常干净整洁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-subway.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-subway2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;天津地铁 9 号线的一辆车, 9 号线大部分路段都在地上&lt;/p&gt;
&lt;h2&gt;天津博物馆&lt;/h2&gt;
&lt;p&gt;天津博物馆非常值得一去, 展品质量都很高, 这里只放我感觉好看的部分照片 😁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-museum1.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;早上 &lt;code&gt;9:00&lt;/code&gt; 开馆, 还没开馆就已经排起了长队&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-museum2.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;镇馆之宝: &lt;a href=&quot;https://www.tjbwg.com/cn/collectionInfo.aspx?Id=2630&quot;&gt;翡翠蝈蝈白菜&lt;/a&gt;, 看起来相当漂亮 👍🏻&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-museum3.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;镇馆之宝: &lt;a href=&quot;https://www.tjbwg.com/cn/collectionInfo.aspx?Id=2345&quot;&gt;太保鼎&lt;/a&gt;, 鼎身高大、造型厚重、比例匀称, 铸造与西周时期, 是商周青铜器的典型代表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-museum4.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.tjbwg.com/cn/collectionInfo.aspx?Id=2673&quot;&gt;翡翠缠枝菊花纹环耳扁盖瓶&lt;/a&gt;, 此瓶由一块大的翡翠料抛开制成一对, 淡翠绿色，间少许淡粉色，色泽均匀柔和，美丽晶莹。此瓶另一看点是工艺高超，雕琢精湛。盖有桃形钮，盖身及瓶两侧镂雕缠枝菊花纹，颈肩镂空花耳上各套一活环，瓶身光素，抛光匀细，椭圆圈足，足下附座，上阴线刻兽面纹。尤其是对花、叶纹饰采用镂雕技法，雕琢更为精细，颇具阿拉伯地域风格，这与翡翠晶莹璀璨的质感交相辉映，为这对高贵典雅的大瓶又增添了一种灵动之感&lt;/p&gt;
&lt;p&gt;翡翠以其深稳凝重、变幻莫测的翠绿色和温润柔美的特性受到举国上下的喜爱，特别是以慈禧为代表的统治阶层的青睐，传说慈禧宁要翡翠饰物而不要金刚石头饰贡品，官员们则投其所好，选上等的翡翠进奉，以求名利，因此翡翠又有“皇家玉”、“玉王”之称。天津博物馆展出的这对翡翠大瓶用料为缅甸翡翠且成对保存至今非常难得。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-museum5.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-museum6.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1860&lt;/code&gt; 年, 英、法、俄强迫清政府签订了《北京条约》, 天津被迫辟为通商口岸; 英国、法国、美国、德国、日本、奥匈帝国、意大利、俄国、比利时等 9 国先后在天津强设了近 15 平方公里的租界地, 相当于天津旧城的8倍。一大批外国冒险家、传教士、富豪纷至沓来，营建洋行、银号、商店、花园、娱乐场、办公楼，开办仓储、航运、进出口贸易，建立医院、学校，盖洋房，造别墅、戏院&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/tianjin-museum7.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-museum8.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-museum9.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/tianjin-museum10.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这身衣服惊艳到我了, 黑蓝配色的衣服非常独特, 古朴华贵&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;非常推荐 &lt;strong&gt;天津博物馆&lt;/strong&gt; 和 &lt;strong&gt;意风区&lt;/strong&gt;, 受限于篇幅还有很多图片没有放上&lt;/li&gt;
&lt;li&gt;天津大部分景点在海河边沿河分布, 例如各种 桥 / 摩天轮 🎡 / 租界区&lt;/li&gt;
&lt;li&gt;个人感觉商业化有点严重, 但这不是问题&lt;/li&gt;
&lt;li&gt;如果不想像我一样一天 2w 步, 可以预约 海河游船 / 双层观光巴士, 或乘坐 电动三轮车&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Github Actions 实现掘金自动签到(juejin-helper)</title><link>http://blog.xiaban.run/posts/2025/juejin-signin-github-actions/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/juejin-signin-github-actions/</guid><description>本文介绍如何使用 juejin-helper Github Actions 实现掘金自动签到</description><pubDate>Wed, 19 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍如何使用 juejin-helper Github Actions 实现掘金自动签到&lt;/p&gt;
&lt;h2&gt;juejin-helper&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;iDerekLi/juejin-helper&quot;}&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/iDerekLi/juejin-helper&quot;&gt;juejin-helper&lt;/a&gt; 是一个掘金自动化 &lt;strong&gt;签到&lt;/strong&gt; / &lt;strong&gt;抽奖&lt;/strong&gt; / &lt;strong&gt;沾喜气&lt;/strong&gt; / &lt;strong&gt;消除 bug&lt;/strong&gt; 的自动化工作流, 通过 &lt;code&gt;Github Actions&lt;/code&gt; 来实现定时执行, 因此 &lt;strong&gt;不需要部署到自己的服务器上&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;直接 &lt;a href=&quot;https://github.com/iDerekLi/juejin-helper/fork&quot;&gt;fork juejin-helper 仓库&lt;/a&gt;, 或者 &lt;a href=&quot;https://github.com/SublimeCT/juejin-helper/fork&quot;&gt;fork 我修改过的仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;仓库 -&amp;gt; &lt;code&gt;Settings&lt;/code&gt; -&amp;gt; &lt;code&gt;Secrets&lt;/code&gt; -&amp;gt; &lt;code&gt;Secrets and variables&lt;/code&gt; -&amp;gt; &lt;code&gt;New repository secret&lt;/code&gt;, 添加Secrets变量如下:&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;COOKIE&lt;/td&gt;
&lt;td&gt;掘金网站 &lt;code&gt;Cookie&lt;/code&gt;, 参考 &lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96cookie&quot;&gt;获取 cookies&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COOKIE_2&lt;/td&gt;
&lt;td&gt;多用户, 当需要同时运行多个掘金用户时所需, 支持最多 &lt;strong&gt;5&lt;/strong&gt; 名用户(即COOKIE + COOKIE_2 - COOKIE_5)&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMAIL_USER&lt;/td&gt;
&lt;td&gt;发件人邮箱地址(需要开启 SMTP)&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMAIL_PASS&lt;/td&gt;
&lt;td&gt;发件人邮箱密码(SMTP密码)&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💡 EMAIL_HOST&lt;/td&gt;
&lt;td&gt;SMTP 服务器地址&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMAIL_TO&lt;/td&gt;
&lt;td&gt;订阅人邮箱地址(收件人). 如需多人订阅使用 &lt;code&gt;, &lt;/code&gt; 分割, 例如: &lt;code&gt;a@163.com, b@qq.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DINGDING_WEBHOOK&lt;/td&gt;
&lt;td&gt;钉钉机器人WEBHOOK&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUSHPLUS_TOKEN&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://www.pushplus.plus/&quot;&gt;Pushplus&lt;/a&gt; 官网申请，支持微信消息推送&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERVERPUSHKEY&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://sct.ftqq.com//&quot;&gt;Server酱&lt;/a&gt; 官网申请，支持微信消息推送&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WEIXIN_WEBHOOK&lt;/td&gt;
&lt;td&gt;企业微信机器人WEBHOOK&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FEISHU_WEBHOOK&lt;/td&gt;
&lt;td&gt;飞书机器人WEBHOOK&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/juejin-signin-secrets.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我们使用邮件通知, 所以只配置了邮箱相关的参数&lt;/p&gt;
&lt;p&gt;:::TIP
其中 &lt;code&gt;EMAIL_HOST&lt;/code&gt; 是我增加的邮箱 SMTP 服务器地址参数, 在原仓库中是直接从邮箱字符串中截取的, 因为我用的是 &lt;a href=&quot;https://qiye.aliyun.com&quot;&gt;阿里邮箱网页端&lt;/a&gt;, 所以 SMTP 服务器地址为 &lt;code&gt;smtp.qiye.aliyun.com&lt;/code&gt;, 需要手动修改, 关于 &lt;strong&gt;免费域名邮箱&lt;/strong&gt; / &lt;strong&gt;使用阿里邮箱发送文件&lt;/strong&gt; 可参考我的这些文章:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;../server-mail/&quot;&gt;服务器请求阿里邮箱服务器发送邮件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../configure-domain-name-mailbox/&quot;&gt;阿里云配置域名邮箱&lt;/a&gt;
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;仓库 -&amp;gt; &lt;code&gt;Actions&lt;/code&gt; -&amp;gt; &lt;code&gt;Auto&lt;/code&gt;, 检查 &lt;code&gt;Workflows&lt;/code&gt; 并启用。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;如何获取Cookie&lt;/h2&gt;
&lt;p&gt;掘金网站Cookie, 打开浏览器，登录 &lt;a href=&quot;https://juejin.cn/&quot;&gt;掘金&lt;/a&gt;, 打开控制台DevTools(快捷键F12) -&amp;gt; Network，复制 cookie, &lt;strong&gt;掘金Cookie有效期约1个月需定期更新.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DevTools截图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/juejin-signin-getcookie.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;修改执行时间&lt;/h2&gt;
&lt;p&gt;在原仓库中, 每天 &lt;code&gt;6:30&lt;/code&gt; 执行, 可以在 &lt;code&gt;.github/workflows/auto.yml&lt;/code&gt; 文件中修改 &lt;code&gt;cron&lt;/code&gt; 表达式, 我改成了每天 &lt;code&gt;8:00&lt;/code&gt; 执行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
  schedule:
    - cron: &quot;0 0 * * *&quot; # 北京时间上午 08:00
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;执行&lt;/h2&gt;
&lt;p&gt;我们可以等待每天到 &lt;code&gt;8:00&lt;/code&gt; 时自动执行, 也可以通过点击 &lt;code&gt;Run workflow&lt;/code&gt; 来立即执行这个 &lt;code&gt;Action&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/juejin-signin-run-workflow.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在执行完毕后, 我们将收到一封通知邮件:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/juejin-signin-email.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;部署到服务器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 使用 rsync 将源码上传到服务器
# 由于阿里云服务器拉取不了 github 仓库代码, 所以只能本地上传
rsync -avz juejin-helper admin@47.69.204.83:/home/admin/projects/
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cd projects/juejin-helper &amp;amp;&amp;amp; pnpm i
cd workflows &amp;amp;&amp;amp; pnpm i
cd ../packages/juejin-helper &amp;amp;&amp;amp; pnpm i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们修改一下环境变量名, 增加 &lt;code&gt;JUEJIN_&lt;/code&gt; 前缀:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const env = process.env || {};

module.exports = {
  /* 掘金Cookie */
  COOKIE: env.JUEJIN_COOKIE || env.COOKIE,
  /* 多用户掘金Cookie, 当有1名以上用户时填写, 支持同时最多可配置5名用户 */
  COOKIE_2: env.COOKIE_2,
  COOKIE_3: env.COOKIE_3,
  COOKIE_4: env.COOKIE_4,
  COOKIE_5: env.COOKIE_5,
  /**
   * 邮箱配置
   * user 发件人邮箱, pass, 发件人密码, to收件人
   */
  EMAIL_USER: env.JUEJIN_EMAIL_USER || env.EMAIL_USER,
  EMAIL_PASS: env.JUEJIN_EMAIL_PASS || env.EMAIL_PASS,
  EMAIL_TO: env.JUEJIN_EMAIL_TO || env.EMAIL_TO,
  EMAIL_HOST: env.JUEJIN_EMAIL_HOST || env.EMAIL_HOST,
  /**
   * 钉钉配置
   * https://open.dingtalk.com/document/robots/custom-robot-access
   */
  DINGDING_WEBHOOK: env.DINGDING_WEBHOOK,
  /**
   * PushPlus配置
   * http://www.pushplus.plus/doc/guide/openApi.html
   */
  PUSHPLUS_TOKEN: env.PUSHPLUS_TOKEN,
  /**
   * 企业微信机器人配置
   * https://developer.work.weixin.qq.com/document/path/91770
   */
  WEIXIN_WEBHOOK: env.WEIXIN_WEBHOOK,
  /**
   * server酱推送key
   * https://sct.ftqq.com/sendkey
   */
  SERVERPUSHKEY: env.SERVERPUSHKEY,
  /**
   * 飞书配置
   */
  FEISHU_WEBHOOK: env.FEISHU_WEBHOOK
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后设置环境变量:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim ~/.profile

# juejin-helper
export JUEJIN_COOKIE=&quot;your-cookie&quot;
export JUEJIN_EMAIL_USER=&quot;no-reply@example.com&quot;
export JUEJIN_EMAIL_PASS=&quot;your-email-password&quot;
export JUEJIN_EMAIL_TO=&quot;your-email@qq.com&quot;
export JUEJIN_EMAIL_HOST=&quot;smtp.qiye.aliyun.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后我们通过 &lt;code&gt;crontab -e&lt;/code&gt; 添加一下这个定时任务:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;crontab -e

# juejin-helper task
15 8 * * * bash -l -c &apos;source /home/admin/.profile &amp;amp;&amp;amp; cd /home/admin/projects/juejin-helper/workflow &amp;amp;&amp;amp; pnpm run checkin&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会在每天 &lt;code&gt;08:15&lt;/code&gt; 执行签到任务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 手动执行测试一下
cd /home/admin/projects/juejin-helper/workflow &amp;amp;&amp;amp; pnpm run checkin
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/iDerekLi/juejin-helper&quot;&gt;juejin-helper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Lit 创建一个 AI 对话组件库 06 补录总结 篇</title><link>http://blog.xiaban.run/posts/2025/hyosan-chat-06-bubble/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/hyosan-chat-06-bubble/</guid><description>距离上一章发布过去了半个月, 没错, 这期间实在是太忙了, 所以这半个月的开发过程并没有做记录</description><pubDate>Tue, 18 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::WARNING
本文是断档式记录, 不记录所有改动, 只作为总结, 具体更新可参考 &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat&quot;&gt;源码&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;举例上一章发布过去了半个月, 没错, 这期间实在是太忙了, 所以这半个月的开发过程并没有做记录 😭, 不过好消息是这个项目顺利发布, 可以在 &lt;a href=&quot;https://www.npmjs.com/package/hyosan-chat&quot;&gt;hyosan-chat - npm&lt;/a&gt; 查看这个 &lt;code&gt;package&lt;/code&gt;, 或直接前往 &lt;code&gt;github&lt;/code&gt; 阅读 &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat&quot;&gt;源码&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;阅读本章内容需要你熟悉 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; / &lt;a href=&quot;../web-compnoents/&quot;&gt;Web Components&lt;/a&gt; / &lt;a href=&quot;https://shoelace.style&quot;&gt;Shoelace&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你对只对组件库感兴趣, 可以直接查看本项目源码: &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat&quot;&gt;hyosan-chat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;如果你对组件库搭建或项目工程化感兴趣, 可以查看前面几章的内容:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-01-create&quot;&gt;搭建篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/hyosan-chat-03-feasibility&quot;&gt;可行性验证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-04-i18n&quot;&gt;国际化&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;demo&lt;/h2&gt;
&lt;p&gt;组件使用了 &lt;code&gt;netlify&lt;/code&gt; 部署 demo 页面, &lt;a href=&quot;snazzy-khapse-06e16b.netlify.app/&quot;&gt;点击查看&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;夜间模式&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;shoelace&lt;/code&gt; 的 &lt;a href=&quot;https://shoelace.style/getting-started/installation#light-and-dark-theme&quot;&gt;文档&lt;/a&gt; 中, 建议使用 &lt;code&gt;link&lt;/code&gt; 标签的形式实现 &lt;code&gt;light&lt;/code&gt; / &lt;code&gt;dark&lt;/code&gt; 样式的引入和切换, 但作为一个组件库, 我们并不能要求所有项目都通过这种方式引入主题样式, 原因是这种方式太过原始, 我们并没有直接提供一种切换主题的方式, 而是让用户直接引入主题的 &lt;code&gt;css&lt;/code&gt;, 既没有做到灵活简单, 也没有扩展性和响应式;&lt;/p&gt;
&lt;p&gt;因此组件内部实现了动态切换主题样式的功能, 在 &lt;code&gt;hyosan-chat&lt;/code&gt; 组件上提供一个主题名称参数 &lt;code&gt;shoelaceTheme&lt;/code&gt;, 并通过 &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat/blob/main/src/utils/HyosanChatTheme.ts&quot;&gt;HyosanChatTheme class&lt;/a&gt; 来实现切换不同的主题&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/utils/HyosanChatTheme.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import shoelaceDarkCss from &apos;@shoelace-style/shoelace/dist/themes/dark.css?inline&apos;
import shoelaceLightCss from &apos;@shoelace-style/shoelace/dist/themes/light.css?inline&apos;

/** shoelace 组件库的主题属性 */
export enum HyosanChatShoelaceTheme {
  shoelaceLight = &apos;sl-theme-light&apos;,
  shoelaceDark = &apos;sl-theme-dark&apos;,
}

/** shoelace 组件库的主题 css 内容 */
export const HyosanChatShoelaceThemes = {
  /** light theme */
  [HyosanChatShoelaceTheme.shoelaceLight]: shoelaceLightCss,
  /** dark theme */
  [HyosanChatShoelaceTheme.shoelaceDark]: shoelaceDarkCss,
}

/**
 * 设置 hyosan-chat 主题
 * @description 用于切换底层组件库 `shoelace` 的主题, `&amp;lt;hyosan-chat&amp;gt;` 组件会根据 `shoelaceTheme` 的值自动切换主题
 */
export class HyosanChatTheme {
  static TAG_ATTRIBUTE = &apos;data-hyosan-chat-theme&apos;
  static getStyleElement() {
    return document.querySelector(`style[${HyosanChatTheme.TAG_ATTRIBUTE}]`)
  }
  static setStyleElement(theme: HyosanChatShoelaceTheme) {
    const styleElement = HyosanChatTheme.getStyleElement()
    const cssText = HyosanChatShoelaceThemes[theme]
    const cssNode = document.createTextNode(cssText)
    if (styleElement) {
      styleElement.innerHTML = &apos;&apos;
      styleElement.appendChild(cssNode)
    } else {
      const style = document.createElement(&apos;style&apos;)
      style.setAttribute(HyosanChatTheme.TAG_ATTRIBUTE, theme)
      style.setAttribute(&apos;type&apos;, &apos;text/css&apos;)
      style.appendChild(cssNode)
      document.head.appendChild(style) // 将包含 shoelace 主题样式的标签插入到 head 中
    }
    HyosanChatTheme._updateThemeClass(theme)
  }
  /** 在根元素上切换 shoelace 主题类 */
  private static _updateThemeClass(theme: HyosanChatShoelaceTheme) {
    document.documentElement.classList.add(theme)
    Object.values(HyosanChatShoelaceTheme).forEach((c) =&amp;gt; {
      console.log(c)
      if (c !== theme) {
        document.documentElement.classList.remove(c)
      }
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/HyosanChat.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  HyosanChatShoelaceTheme,
  HyosanChatTheme,
} from &apos;@/utils/HyosanChatTheme&apos;

@customElement(&apos;hyosan-chat&apos;)
export class HyosanChat extends ShoelaceElement {
  // ...
  /** shoelace 主题, 可用于切换夜间模式 */
  @property({ reflect: true })
  shoelaceTheme: HyosanChatShoelaceTheme = HyosanChatShoelaceTheme.shoelaceLight 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关改动可参考: &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat/commit/f12f97ff972875adf1c305c2695d36d6c8737c72&quot;&gt;feat: 增加切换主题功能&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;框架适配&lt;/h2&gt;
&lt;p&gt;最初在做调研时, 发现 &lt;code&gt;web components&lt;/code&gt; 是一个 &lt;strong&gt;原生技术&lt;/strong&gt;, 这就代表了它适用于所有框架, 包括 &lt;code&gt;vue&lt;/code&gt; / &lt;code&gt;react&lt;/code&gt; / &lt;code&gt;angular&lt;/code&gt; / &lt;code&gt;...&lt;/code&gt;, 现在组件的基础功能开发完成, 是不是就意味着可以直接在使用这些框架的项目中使用呢? 是也不是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义标签没有声明属性和事件&lt;/li&gt;
&lt;li&gt;IDE 无法识别自定义标签&lt;/li&gt;
&lt;li&gt;各个框架的模板语法不同&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一方面 &lt;code&gt;web components&lt;/code&gt; 的确是原生技术, 它使用的技术 &lt;strong&gt;不依赖于任何框架&lt;/strong&gt;, 可以直接使用; 但另一方面, 在一个现代化的前端开发环境中, &lt;strong&gt;&lt;code&gt;TypeScript&lt;/code&gt; 是必不可少的一环&lt;/strong&gt;, &lt;code&gt;web components&lt;/code&gt; 并 &lt;strong&gt;没有提供定义自定义标签类型的功能&lt;/strong&gt;, 编辑器更不会知道当前项目有哪些自定义标签, 以及标签上有哪些属性, 而且更加复杂的是, 在不同的框架中 &lt;code&gt;HTML&lt;/code&gt; 的语法都不一样, 例如 &lt;code&gt;vue&lt;/code&gt; 的模板语法与 &lt;code&gt;jsx&lt;/code&gt; 的模板语法不同, &lt;code&gt;angular&lt;/code&gt; 的模板语法与前两者也有差别 ...&lt;/p&gt;
&lt;p&gt;&amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; viewBox=&quot;0 0 244 244&quot; version=&quot;1.1&quot; alt=&quot;open-wc&quot; class=&quot;logo&quot;&amp;gt;&amp;lt;defs&amp;gt;&amp;lt;linearGradient x1=&quot;50%&quot; y1=&quot;0%&quot; x2=&quot;50%&quot; y2=&quot;100%&quot; id=&quot;linearGradient-1&quot;&amp;gt;&amp;lt;stop stop-color=&quot;#9B00FF&quot; offset=&quot;0%&quot;/&amp;gt;&amp;lt;stop stop-color=&quot;#0077FF&quot; offset=&quot;100%&quot;/&amp;gt;&amp;lt;/linearGradient&amp;gt;&amp;lt;g stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot; id=&quot;logo&quot;&amp;gt;&amp;lt;path d=&quot;M205.639259,176.936244 C207.430887,174.217233 209.093339,171.405629 210.617884,168.510161 M215.112174,158.724316 C216.385153,155.50304 217.495621,152.199852 218.433474,148.824851 M220.655293,138.874185 C221.231935,135.482212 221.637704,132.03207 221.863435,128.532919 M222,118.131039 C221.860539,114.466419 221.523806,110.85231 221.000113,107.299021 M218.885321,96.8583653 C218.001583,93.4468963 216.942225,90.1061026 215.717466,86.8461994 M211.549484,77.3039459 C209.957339,74.1238901 208.200597,71.0404957 206.290425,68.0649233 M200.180513,59.5598295 C181.848457,36.6639805 153.655709,22 122.036748,22 C66.7879774,22 22,66.771525 22,122 C22,177.228475 66.7879774,222 122.036748,222 C152.914668,222 180.52509,208.015313 198.875424,186.036326&quot; stroke=&quot;url(#linearGradient-1)&quot; stroke-width=&quot;42.0804674&quot;/&amp;gt;&amp;lt;/g&amp;gt;&amp;lt;/defs&amp;gt;&amp;lt;use xlink:href=&quot;#logo&quot;/&amp;gt;&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;为此社区出现了一个 &lt;a href=&quot;https://custom-elements-manifest.open-wc.org/&quot;&gt;custom-elements-manifest&lt;/a&gt; 的库, 用于为不同的框架提供 &lt;code&gt;manifest&lt;/code&gt; 信息, 简而言之它可以 &lt;strong&gt;让编辑器和不同的框架准确的识别自定义标签&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于自定义标签的类型定义, 通过在代码中添加 &lt;a href=&quot;https://jsdoc.app/&quot;&gt;jsdoc&lt;/a&gt; 注释, 并通过 &lt;a href=&quot;https://custom-elements-manifest.open-wc.org/&quot;&gt;custom-elements-manifest&lt;/a&gt; 生成 &lt;code&gt;vscode.html-custom-data.json&lt;/code&gt;(用于 &lt;code&gt;vscode&lt;/code&gt;) / &lt;code&gt;web-types.json&lt;/code&gt;(用于 &lt;code&gt;JetBrains IDE&lt;/code&gt;) 文件来实现&lt;/li&gt;
&lt;li&gt;对于每个框架:
&lt;ul&gt;
&lt;li&gt;vue: 通过 &lt;a href=&quot;https://www.npmjs.com/package/custom-element-vuejs-integration&quot;&gt;custom-element-vuejs-integration&lt;/a&gt; 生成组件的类型定义&lt;/li&gt;
&lt;li&gt;react: 通过 &lt;a href=&quot;https://www.npmjs.com/package/custom-element-react-wrappers&quot;&gt;custom-element-react-wrappers&lt;/a&gt; 生成组件包装器&lt;/li&gt;
&lt;li&gt;angular: 无需生成文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体配置可参考: &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat/blob/main/custom-elements-manifest.config.mjs&quot;&gt;custom-elements-manifest.config.mjs&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;示例项目&lt;/h2&gt;
&lt;p&gt;配置好不同框架的适配之后, 我们直接创建 demo 项目:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/hyosan-chat-demo-projects.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat-vue-demo&quot;&gt;hyosan-chat-vue-demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat-react-demo&quot;&gt;hyosan-chat-react-demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat-angular-demo&quot;&gt;hyosan-chat-angular-demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat-vanilla-demo&quot;&gt;hyosan-chat-vanilla-demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;发布&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
+    &quot;npm:login&quot;: &quot;pnpm login --registry=https://registry.npmjs.org&quot;,
+    &quot;npm:publish&quot;: &quot;pnpm run build:lib &amp;amp;&amp;amp; pnpm publish --registry=https://registry.npmjs.org&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里为什么要指定 &lt;code&gt;registry&lt;/code&gt; 呢? 因为在国内的网络环境下, 一般会设置淘宝的 npm 镜像, 或内部私有镜像, 但在发布时是需要连接到官方源的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 只需执行一次, 之后可以直接执行 pnpm run npm:publish
pnpm run npm:login

# 发布新版本包
pnpm run npm:publish
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat&quot;&gt;源码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>配置 markdown-it 解析</title><link>http://blog.xiaban.run/posts/2025/markdown-it/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/markdown-it/</guid><description>最近在做一个 AI 对话组件, 当收到消息时需要对 markdown 内容进行实时渲染, 所以需要配置 markdown-it 解析, 我们将参考 vitepress 的 markdown-it 配置</description><pubDate>Sun, 09 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在做一个 &lt;code&gt;AI&lt;/code&gt; 对话组件, 当收到消息时需要对 &lt;code&gt;markdown&lt;/code&gt; 内容进行实时渲染, 所以需要配置 &lt;a href=&quot;https://www.npmjs.com/package/markdown-it&quot;&gt;markdown-it&lt;/a&gt; 解析,  我们将参考 &lt;code&gt;vitepress&lt;/code&gt; 的 &lt;code&gt;markdown-it&lt;/code&gt; 配置&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;由于对 markdown 渲染比较陌生, 所以直接使用了 &lt;a href=&quot;https://www.npmjs.com/package/marked&quot;&gt;marked&lt;/a&gt; 进行了渲染, 但是发现 &lt;code&gt;marked&lt;/code&gt; 的生态及互联网上可参考的配置并不多, 倒是 &lt;a href=&quot;https://www.npmjs.com/package/markdown-it&quot;&gt;markdown-it&lt;/a&gt; 有比较丰富的生态及配置, &lt;a href=&quot;https://vitepress.dev/&quot;&gt;vitepress&lt;/a&gt; 就是使用 &lt;a href=&quot;https://www.npmjs.com/package/markdown-it&quot;&gt;markdown-it&lt;/a&gt; 进行渲染的, 我们将参考 &lt;code&gt;vitepress&lt;/code&gt; 的 &lt;code&gt;markdown-it&lt;/code&gt; 配置&lt;/p&gt;
&lt;h2&gt;封装 API&lt;/h2&gt;
&lt;p&gt;由于我们是在自己的项目里使用, 对我来说, 我是在组件库中使用的, 所以我们将 &lt;code&gt;markdown&lt;/code&gt; 渲染功能封装成一个 API, 然后在组件中使用, 代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import MarkdownIt from &apos;markdown-it-async&apos;

const md = MarkdownIt()

// ...

/**
 * 将 markdown 字符串通过 markdown-it 渲染为 html 字符串
 * @param content markdown 字符串
 * @returns html 字符串
 */
export async function renderMarkdown(content: string) {
	const htmlContent = await md.render(content)
	return htmlContent
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对外提供一个 &lt;code&gt;renderMarkdown&lt;/code&gt; 方法, 接收 &lt;code&gt;content&lt;/code&gt; 参数, 返回 &lt;code&gt;html&lt;/code&gt; 内容&lt;/p&gt;
&lt;p&gt;:::tip
这里我们使用了异步方法, 在实际使用中, 当我们需要渲染大量的 &lt;code&gt;markdown&lt;/code&gt; 时, 可以使用异步方法, 可以避免阻塞主线程
:::&lt;/p&gt;
&lt;h2&gt;安装 markdown-it&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm i markdown-it-async
pnpm i -D @types/markdown-it
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考 vitepress 源码&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;首先我们找到 &lt;a href=&quot;https://github.com/vuejs/vitepress&quot;&gt;vitepress 仓库&lt;/a&gt;, 然后直接查看 &lt;code&gt;packages.json&lt;/code&gt;, 发现 &lt;code&gt;vitepress&lt;/code&gt; 使用了 &lt;code&gt;markdown-it&lt;/code&gt; 进行了渲染&lt;/li&gt;
&lt;li&gt;搜索 &lt;code&gt;markdown-it&lt;/code&gt;, 在 &lt;a href=&quot;https://github.com/search?q=repo%3Avuejs%2Fvitepress%20markdown-it&amp;amp;type=code&quot;&gt;搜索结果&lt;/a&gt; 我们可以看到所有包含 &lt;code&gt;markdown-it&lt;/code&gt; 的源码文件, 但这里面包含了大量 &lt;code&gt;md&lt;/code&gt; 文件, 实际上我们只需要看 &lt;code&gt;ts&lt;/code&gt; 文件, 所以我们将搜索框内容改为 &lt;code&gt;repo:vuejs/vitepress path:*.ts markdown-it&lt;/code&gt;, 这样就可以只搜索 &lt;code&gt;ts&lt;/code&gt; 文件了&lt;/li&gt;
&lt;li&gt;在 &lt;a href=&quot;https://github.com/search?q=repo%3Avuejs%2Fvitepress+path%3A*.ts+markdown-it&amp;amp;type=code&quot;&gt;搜索结果&lt;/a&gt; 中, 可以看到 &lt;code&gt;vitepress&lt;/code&gt; 将关于 &lt;code&gt;markdown&lt;/code&gt; 渲染的功能都放到了 &lt;a href=&quot;https://github.com/vuejs/vitepress/tree/8aad617446c03d39a65a0b21e9fce43bc484af1e/src/node/markdown&quot;&gt;src/node/markdown&lt;/a&gt; 文件中, 入口文件就是 &lt;a href=&quot;https://github.com/vuejs/vitepress/blob/8aad617446c03d39a65a0b21e9fce43bc484af1e/src/node/markdown/markdown.ts&quot;&gt;src/mode/markdown/markdown.ts&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vitepress-source-markdown.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码中包含一些 &lt;code&gt;@mdit-vue/*&lt;/code&gt; 包, 这些是将 &lt;code&gt;markdown&lt;/code&gt; 转换为 &lt;code&gt;vue&lt;/code&gt; 代码的包, 由于 &lt;code&gt;vitepress&lt;/code&gt; 的渲染产物是 vue 代码, 所以我们并不能直接使用, 但我们可以挑选用得到的相关的插件或依赖, 并加入到我们的项目中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i markdown-it-async markdown-it-highlightjs markdown-it-link-attributes markdown-it-mathjax3
pnpm i -D @types/markdown-it-link-attributes
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { MarkdownItAsync } from &apos;markdown-it-async&apos;
import highlight from &apos;markdown-it-highlightjs&apos;
import CodeBlockWrapper from &apos;./CodeBlockWrapper&apos;
import linkAttr from &apos;markdown-it-link-attributes&apos;
import mathjax3 from &apos;markdown-it-mathjax3&apos;

/** markdown-it 实例 */
let _md: MarkdownItAsync

/** 获取并初始化 markdown-it 实例 */
export async function getMarkdownItInstance(): Promise&amp;lt;MarkdownItAsync&amp;gt; {
  if (_md) return _md
  _md = new MarkdownItAsync()
  // 代码高亮
  _md.use(highlight)
  // 代码块使用自定义组件包裹
  _md.use(CodeBlockWrapper)
  // 链接在新窗口打开
  _md.use(linkAttr, { attrs: { target: &apos;_blank&apos;, rel: &apos;noopener&apos;} })
  // 渲染数学公式
  _md.use(mathjax3, {
    tex: {
      tags: &apos;ams&apos;,
      inlineMath: [
        // start/end delimiter pairs for in-line math
        [&quot;$&quot;, &quot;$&quot;],
        [&quot;\\(&quot;, &quot;\\)&quot;]
      ],
      displayMath: [
        // start/end delimiter pairs for display math
        [&quot;$$&quot;, &quot;$$&quot;],
        [&quot;\\[&quot;, &quot;\\]&quot;]
      ],
    }
  })
  return _md
}

/**
 * 将 markdown 字符串通过 markdown-it 渲染为 html 字符串
 * @param content markdown 字符串
 * @returns html 字符串
 */
export async function renderMarkdown(content: string) {
  const processedContent = getProcessedContent(content)
  const md = await getMarkdownItInstance()
	const htmlContent = await md.renderAsync(processedContent)
	return htmlContent
}

/**
 * 将 markdown 字符串进行预处理, 将 `\\[` 和 `\\]` 替换为 `$$`
 * @param content markdown 字符串
 */
export function getProcessedContent(content: string) {
  return content.replace(/\\\[|\\\]/g, &apos;$$&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上 &lt;code&gt;markdown-it&lt;/code&gt; 还有很多插件, 可以通过在 &lt;code&gt;npmjs.com&lt;/code&gt; 搜索 &lt;code&gt;markdown-it&lt;/code&gt; &lt;a href=&quot;https://www.npmjs.com/search?q=markdown-it&quot;&gt;查看&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;让我们看一下所有的依赖:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;markdown-it-highlightjs&lt;/code&gt;: 用于语法高亮, 直接将样式内联, 省去了引入 &lt;code&gt;css&lt;/code&gt; 的步骤, &lt;em&gt;但是主题样式还是需要引入的, 例如 &lt;code&gt;import &apos;highlight.js/styles/github-dark.min.css&apos;&lt;/code&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;markdown-it-link-attributes&lt;/code&gt;: 用于添加链接的属性, 例如 &lt;code&gt;target=&quot;_blank&quot;&lt;/code&gt;, 这个对于对话组件是必须的, 因为我们希望用户点击链接后, 会在新窗口中打开&lt;/li&gt;
&lt;li&gt;&lt;code&gt;markdown-it-mathjax3&lt;/code&gt;: 用于渲染数学公式, &lt;em&gt;注意, 这里需要对一些特殊的语法进行替换, 例如 &lt;code&gt;\\[ ... \\]&lt;/code&gt;, 在代码中的 &lt;code&gt;getProcessedContent&lt;/code&gt; 中进行了替换处理&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码块自定义元素&lt;/h2&gt;
&lt;p&gt;在以上代码中我们还写了一个自定义插件 &lt;code&gt;CodeBlockWrapper&lt;/code&gt;, 用于将代码块包裹成一个自定义元素, 这样我们就可以对每个代码块进行处理, 比如: &lt;strong&gt;添加复制按钮&lt;/strong&gt; / &lt;strong&gt;美化样式&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { MarkdownItAsync } from &quot;markdown-it-async&quot;;
import hljs from &quot;highlight.js&quot;;

/**
 * 自定义插件: 添加复制按钮
 * @param md markdown-it 实例
 */
export default function CodeBlockWrapper(md: MarkdownItAsync) {
  const defaultRenderer = md.renderer.rules.fence || ((tokens, idx, options, env, self) =&amp;gt; self.renderToken(tokens, idx, options));

  md.renderer.rules.fence = (tokens, idx, options, env, self) =&amp;gt; {
    const token = tokens[idx];
    const code = token.content.trim();
    const lang = token.info.trim();

    let highlightedCode = &apos;&apos;;
    if (lang &amp;amp;&amp;amp; hljs.getLanguage(lang)) {
      highlightedCode = hljs.highlight(code, { language: lang }).value;
    } else {
      highlightedCode = md.utils.escapeHtml(code);
    }

    // const copyButtonHtml = &apos;&amp;lt;button class=&quot;copy-button&quot; title=&quot;Copy to clipboard&quot;&amp;gt;Copy&amp;lt;/button&amp;gt;&apos;;
    const preHtml = `&amp;lt;pre class=&quot;hljs&quot;&amp;gt;&amp;lt;code&amp;gt;${highlightedCode}&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;`;
    // const html = `&amp;lt;div class=&quot;code-block&quot;&amp;gt;${copyButtonHtml}${preHtml}&amp;lt;/div&amp;gt;`;
    const html = `&amp;lt;hyosan-chat-code-block-wrapper language=&quot;${lang}&quot;&amp;gt;${preHtml}&amp;lt;/hyosan-chat-code-block-wrapper&amp;gt;`

    return html;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们将代码包裹在 &lt;code&gt;&amp;lt;hyosan-chat-code-block-wrapper&amp;gt;&lt;/code&gt; 中, 然后传入 &lt;code&gt;language&lt;/code&gt; 参数&lt;/p&gt;
&lt;p&gt;接着创建自定义组件(这里使用了 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 来编写自定义组件), 创建 &lt;code&gt;src/components/hyosan-chat-code-block-wrapper&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ShoelaceElement from &apos;@/internal/shoelace-element&apos;
import { LocalizeController } from &apos;@/utils/localize&apos;
import { css, html } from &apos;lit&apos;
import { customElement, property, state } from &apos;lit/decorators.js&apos;

/** 发送 组件 */
@customElement(&apos;hyosan-chat-code-block-wrapper&apos;)
export class HyosanChatCodeBlockWrapper extends ShoelaceElement {
	static styles? = css`
		:host {
			display: block;
			margin: 1rem 0;
		}
		.code-block-container {
			display: block;
			border-radius: var(--hy-container-radius);
			border: 1px solid rgba(60, 60, 60, 0.1);
			background-color: var(--sl-color-neutral-500);
			header {
				margin: 0.3rem 0.5rem;
				color: #EEE;
				display: flex;
				justify-content: space-between;
				button {
					display: block;
					cursor: pointer;
				}
			}
		}
		::slotted(pre) {
			padding: var(--hy-container-padding);
			margin: 0;
			border-bottom-left-radius: var(--hy-container-radius);
			border-bottom-right-radius: var(--hy-container-radius);
		}
  `

	@property({ type: String })
	language = &apos;javascript&apos;

	@state()
	private _copyButtonContent = &apos;&apos;

	/** 本地化控制器 */
	private _localize = new LocalizeController(this)

	private _handleCopy() {
		const pre = this.querySelector(&apos;pre&apos;)
		if (pre) {
			const text = pre.textContent
			if (text) {
				navigator.clipboard.writeText(text)
				this._copyButtonContent = this._localize.term(&apos;copySuccessfully&apos;)
				setTimeout(() =&amp;gt; {
					this._copyButtonContent = this._localize.term(&apos;copy&apos;)
					this.requestUpdate()
				}, 2000)
			}
		}
	}

	render() {
		const copyButtonContent = this._copyButtonContent || this._localize.term(&apos;copy&apos;)
		return html`
      &amp;lt;div class=&quot;code-block-container&quot;&amp;gt;
				&amp;lt;header&amp;gt;
					&amp;lt;div class=&quot;lang&quot;&amp;gt;${this.language}&amp;lt;/div&amp;gt;
					&amp;lt;div class=&quot;button-group&quot;&amp;gt;
						&amp;lt;button @click=${this._handleCopy}&amp;gt;${copyButtonContent}&amp;lt;/button&amp;gt;
					&amp;lt;/div&amp;gt;
				&amp;lt;/header&amp;gt;
				&amp;lt;main&amp;gt;
					&amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
				&amp;lt;/main&amp;gt;
      &amp;lt;/div&amp;gt;
    `
	}
}

declare global {
	interface HTMLElementTagNameMap {
    &apos;hyosan-chat-code-block-wrapper&apos;: HyosanChatCodeBlockWrapper
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终效果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/code-block-wrapper-screenshot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/markdown-it&quot;&gt;markdown-it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitepress.dev/&quot;&gt;vitepress&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shiki.tmrs.site/&quot;&gt;shiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Lit 创建一个 AI 对话组件库 05 conversations 篇</title><link>http://blog.xiaban.run/posts/2025/hyosan-chat-05-conversations/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/hyosan-chat-05-conversations/</guid><description>前面几章花了太多时间搭建和配置项目, 从本章开始我们先写点功能, 就从左侧的对话列表开始</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前面我们从 &lt;a href=&quot;../web-compnoents/&quot;&gt;Web Components&lt;/a&gt; 开始看起, 接着学习了 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt;, 然后开始搭建项目, 引入各种工程化依赖, 增加打包配置并进行了可行性测试, 始终没有涉及实际的功能, 是因为我希望能更全面的学习 &lt;code&gt;Web Components&lt;/code&gt;, 组件库只是我们学习的最终成果, 成果固然重要, 但 学习不同的技术 / 翻阅文档 / 阅读源码 的经历更加宝贵&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;阅读本章内容需要你熟悉 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; / &lt;a href=&quot;../web-compnoents/&quot;&gt;Web Components&lt;/a&gt; / &lt;a href=&quot;https://shoelace.style&quot;&gt;Shoelace&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你对只对组件库感兴趣, 可以直接查看本项目源码: &lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat&quot;&gt;hyosan-chat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;如果你对组件库搭建或项目工程化感兴趣, 可以查看前面几章的内容:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-01-create&quot;&gt;搭建篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/hyosan-chat-03-feasibility&quot;&gt;可行性验证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-04-i18n&quot;&gt;国际化&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建一个对话列表&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://x.ant.design/index-cn&quot;&gt;ant-design-x&lt;/a&gt; 是一个优秀的 AI 对话组件库, 我们的组件库将参考 &lt;a href=&quot;https://x.ant.design/index-cn&quot;&gt;ant-design-x&lt;/a&gt; 的 &lt;code&gt;UI&lt;/code&gt; 设计 / &lt;code&gt;API&lt;/code&gt; 设计, 并实现部分基础的功能&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;732px&quot; src=&quot;https://x.ant.design/~demos/docs-playground-independent-demo-independent&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;以上是 &lt;a href=&quot;https://x.ant.design/index-cn&quot;&gt;ant-design-x&lt;/a&gt; 的 &lt;code&gt;UI&lt;/code&gt;, 我们要做的组件最终效果也是这样的, &lt;code&gt;ant-design-x&lt;/code&gt; 做的足够好, 但它对组件进行了非常细致的拆分, 导致在使用时虽然能做到足够的灵活可扩展性, 但也增加了 &lt;code&gt;API&lt;/code&gt; 复杂度, 我们将在实现部分功能的基础上, 简化 &lt;code&gt;API&lt;/code&gt;, 本章先从 &lt;code&gt;conversations&lt;/code&gt; 组件开始, 参考 &lt;a href=&quot;https://x.ant.design/components/conversations-cn?theme=dark&quot;&gt;Conversations 管理对话&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;ant-design-x API&lt;/h2&gt;
&lt;p&gt;通过阅读源码, 找到了 &lt;code&gt;Conversations&lt;/code&gt; 组件的 &lt;code&gt;API&lt;/code&gt;: &lt;a href=&quot;https://github.com/ant-design/x/blob/bc9224a44629ee991c57d27303a403553620570c/components/conversations/interface.ts#L11&quot;&gt;interface.ts&lt;/a&gt;, 我们将其复制到 &lt;code&gt;src/types/conversations.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { AnyObject } from &apos;./helpers&apos;

type GroupType = string

/**
 * @desc 会话数据
 * @descEN Conversation data
 */
export interface Conversation extends AnyObject {
	/**
	 * @desc 唯一标识
	 * @descEN Unique identifier
	 */
	key: string

	/**
	 * @desc 会话名称
	 * @descEN Conversation name
	 */
	label: string

	/**
	 * @desc 会话时间戳
	 * @descEN Conversation timestamp
	 */
	timestamp?: number

	/**
	 * @desc 会话分组类型，与 {@link ConversationsProps.groupable} 联动
	 * @descEN Conversation type
	 */
	group?: GroupType

	/**
	 * @desc 会话图标
	 * @descEN conversation icon
	 */
	icon?: string

	/**
	 * @desc 是否禁用
	 * @descEN Whether to disable
	 */
	disabled?: boolean
}

export type GroupSorter = Parameters&amp;lt;GroupType[][&apos;sort&apos;]&amp;gt;[0]

export interface Groupable {
	/**
	 * @desc 分组排序函数
	 * @descEN Group sorter
	 */
	sort?: GroupSorter
	/**
	 * @desc 自定义分组标签渲染
	 * @descEN Semantic custom rendering
	 */
	title?: string
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;controllers&lt;/h2&gt;
&lt;p&gt;在之前阅读 &lt;a href=&quot;https://shoelace.style&quot;&gt;shoelace&lt;/a&gt; 源码的时候, 发现在 &lt;a href=&quot;https://github.com/shoelace-style/shoelace/blob/next/src/internal&quot;&gt;src/internal&lt;/a&gt; 中有许多 &lt;code&gt;controllers&lt;/code&gt;, 而且只依赖 &lt;code&gt;Lit&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;Controller&lt;/code&gt; 可以直接调用组件的生命周期 hooks, 也就可以直接更新组件&lt;/strong&gt;, 详见 &lt;a href=&quot;https://lit.dev/docs/composition/controllers/&quot;&gt;Reactive Controllers&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们将目前用到的 &lt;code&gt;slot.ts&lt;/code&gt; 复制到项目中(&lt;code&gt;src/internal/slot.ts&lt;/code&gt;), 并且在 &lt;code&gt;hyosan-chat.ts&lt;/code&gt; 中引入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ import { HasSlotController } from &apos;@/internal/slot&apos;
export class HyosanChat extends ShoelaceElement {
+	private readonly hasSlotController = new HasSlotController(
+		this,
+		&apos;conversations&apos;,
+		&apos;conversations-header&apos;,
+		&apos;conversations-footer&apos;,
+	)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;HasSlotController&lt;/code&gt; 实现了对于 &lt;code&gt;slot&lt;/code&gt; 的检测, 我们可以在 render 中判断 &lt;code&gt;slot&lt;/code&gt; 是否存在, 并以此实现没有传 &lt;code&gt;slot&lt;/code&gt; 时使用默认的元素渲染:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	render() {
+		const hasConversationsSlot = this.hasSlotController.test(&apos;conversations&apos;)
+		const hasConversationsHeaderSlot = this.hasSlotController.test(
+			&apos;conversations-header&apos;,
+		)
+		/** 会话列表 header */
+		const conversationsHeader = hasConversationsHeaderSlot
+			? html`&amp;lt;slot name=&quot;conversations-header&quot;&amp;gt;&amp;lt;/slot&amp;gt;`
+			: html`&amp;lt;hyosan-chat-conversations-header slot=&quot;conversations-header&quot;&amp;gt;&amp;lt;/hyosan-chat-conversations-header&amp;gt;`
+		/** 会话列表 */
+		const conversations = hasConversationsSlot
+			? html`&amp;lt;slot name=&quot;conversations&quot;&amp;gt;${conversationsHeader}&amp;lt;slot name=&quot;conversations-footer&quot;&amp;gt;&amp;lt;/slot&amp;gt;&amp;lt;/slot&amp;gt;`
+			: html`&amp;lt;hyosan-chat-conversations .items=${this.items}&amp;gt;${conversationsHeader}&amp;lt;slot name=&quot;conversations-footer&quot;&amp;gt;&amp;lt;/slot&amp;gt;&amp;lt;/hyosan-chat-conversations&amp;gt;`
    // ...
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的渲染逻辑是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果传入了名为 &lt;code&gt;conversations&lt;/code&gt; 的 &lt;code&gt;slot&lt;/code&gt;, 则使用 &lt;code&gt;slot&lt;/code&gt; 中的内容, 否则使用默认的 &lt;code&gt;hyosan-chat-conversations&lt;/code&gt; 组件渲染&lt;/li&gt;
&lt;li&gt;如果传入了名为 &lt;code&gt;conversations-header&lt;/code&gt; 的 &lt;code&gt;slot&lt;/code&gt;, 则使用 &lt;code&gt;slot&lt;/code&gt; 中的内容, 否则使用默认的 &lt;code&gt;hyosan-chat-conversations-header&lt;/code&gt; 组件渲染&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分割面板&lt;/h2&gt;
&lt;p&gt;组件整体的布局是左侧会话列表, 右侧消息列表, 下面我们使用 &lt;a href=&quot;http://shoelace.style/components/split-panel#snapping&quot;&gt;sl-split-panel&lt;/a&gt; 来实现左右布局:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/components/hyosan-chat.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    return html`
- 	  &amp;lt;h2&amp;gt;${this.message}&amp;lt;/h2&amp;gt;
-		  &amp;lt;sl-button variant=&quot;primary&quot;&amp;gt;Hello Shoelace&amp;lt;/sl-button&amp;gt;
-		  &amp;lt;p&amp;gt;${this._locailze.term(&apos;test&apos;)}&amp;lt;/p&amp;gt;
+			&amp;lt;sl-split-panel snap=&quot;${this.panelSnap}&quot; position=&quot;${this.panelPosition}&quot;&amp;gt;
+				&amp;lt;div
+					slot=&quot;start&quot;
+					style=&quot;height: 100%; overflow-y: auto; background: var(--sl-color-neutral-50);&quot;
+				&amp;gt;
+					&amp;lt;!-- 管理会话 --&amp;gt;
+					${conversations}
+				&amp;lt;/div&amp;gt;
+				&amp;lt;div
+					slot=&quot;end&quot;
+					style=&quot;height: 100%;&quot;
+				&amp;gt;
+					&amp;lt;!-- 对话气泡 --&amp;gt;
+					&amp;lt;hyosan-chat-bubble&amp;gt;&amp;lt;/hyosan-chat-bubble&amp;gt;
+				&amp;lt;/div&amp;gt;
+			&amp;lt;/sl-split-panel&amp;gt;
    `
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们将 &lt;code&gt;snap&lt;/code&gt; / &lt;code&gt;position&lt;/code&gt; 作为参数作为组件的属性, 接受这两个属性并传入 &lt;code&gt;sl-split-panel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conversations&lt;/code&gt; 组件是左侧的会话列表, 我们将在本章实现这个组件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;+	/**
+	 * 分割面板的可捕捉位置
+	 * @example &apos;25% 50%&apos;
+	 * @see https://shoelace.style/components/split-panel#snapping
+	 */
+	@property({ reflect: true })
+	panelSnap = &apos;25%&apos;
+
+	/**
+	 * 分隔线与主面板边缘的当前位置(百分比, 0-100), 默认为容器初始大小的 `50%`
+	 * @example 25
+	 * @see https://shoelace.style/components/split-panel#initial-position
+	 */
+	@property({ reflect: true, type: Number })
+	panelPosition = 25
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;vscode snippets&lt;/h2&gt;
&lt;p&gt;在创建 &lt;code&gt;Lit Components&lt;/code&gt; 的时候, 我们总是将每个组件都有的代码复制到新组件中, 如果能直接生成模板代码让我们使用就好了; 实际上 &lt;code&gt;vscode&lt;/code&gt; 已经提供了 &lt;code&gt;snippets&lt;/code&gt; 功能, 我们可以创建一个 &lt;code&gt;snippets&lt;/code&gt; 模板, 然后在 &lt;code&gt;vscode&lt;/code&gt; 中使用 &lt;code&gt;snippets&lt;/code&gt; 快速创建组件:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.vscode/hy.code-snippets&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	// Place your hyosan-chat 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 
	// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 
	// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 
	// used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 
	// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 
	// Placeholders with the same ids are connected.
	// Example:
	// &quot;Print to console&quot;: {
	// 	&quot;scope&quot;: &quot;javascript,typescript&quot;,
	// 	&quot;prefix&quot;: &quot;log&quot;,
	// 	&quot;body&quot;: [
	// 		&quot;console.log(&apos;$1&apos;);&quot;,
	// 		&quot;$2&quot;
	// 	],
	// 	&quot;description&quot;: &quot;Log output to console&quot;
	// }
	&quot;Hyosan Component&quot;: {
		&quot;prefix&quot;: &quot;hy-component&quot;,
		&quot;body&quot;: [
      &quot;import ShoelaceElement from &apos;@/internal/shoelace-element&apos;&quot;,
      &quot;// import { LocalizeController } from &apos;@shoelace-style/localize&apos;&quot;,
      &quot;import { css, html } from &apos;lit&apos;&quot;,
      &quot;import { customElement, property } from &apos;lit/decorators.js&apos;&quot;,
      &quot;&quot;,
      &quot;/** $0 组件 */&quot;,
      &quot;@customElement(&apos;${TM_FILENAME_BASE}&apos;)&quot;,
      &quot;export class ${TM_FILENAME_BASE/(^|\\-)(\\w)/${2:/upcase}/g} extends ShoelaceElement {&quot;,
      &quot;  static styles? = css``&quot;,
      &quot;&quot;,
      &quot;  // /** 本地化控制器 */&quot;,
      &quot;  // private _localize = new LocalizeController(this)&quot;,
      &quot;&quot;,
      &quot;  @property({ reflect: true })&quot;,
      &quot;  message = &apos;&apos;&quot;,
      &quot;  render() {&quot;,
      &quot;    return html`&quot;,
      &quot;      &amp;lt;div&amp;gt;${this.message}&amp;lt;/div&amp;gt;&quot;,
      &quot;    `&quot;,
      &quot;  }&quot;,
      &quot;}&quot;,
      &quot;&quot;,
      &quot;declare global {&quot;,
      &quot;  interface HTMLElementTagNameMap {&quot;,
      &quot;    &apos;${TM_FILENAME_BASE}&apos;: ${TM_FILENAME_BASE/(^|\\-)(\\w)/${2:/upcase}/g}&quot;,
      &quot;  }&quot;,
      &quot;}&quot;,
      &quot;&quot;
		],
		&quot;description&quot;: &quot;Generate a Hyosan component based on the filename&quot;
	},
	&quot;Hyosan Component Property&quot;: {
		&quot;prefix&quot;: &quot;hy-component-property&quot;,
		&quot;body&quot;: [
			&quot;/** $2 */&quot;,
			&quot;@property({ reflect: true })&quot;,
			&quot;$1 = &apos;&apos;&quot;
		]
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建组件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/components/hyosan-chat-conversations-header.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ShoelaceElement from &apos;@/internal/shoelace-element&apos;
// import { LocalizeController } from &apos;@shoelace-style/localize&apos;
import { css, html } from &apos;lit&apos;
import { customElement, property } from &apos;lit/decorators.js&apos;

/** 会话列表头部 组件 */
@customElement(&apos;hyosan-chat-conversations-header&apos;)
export class HyosanChatConversationsHeader extends ShoelaceElement {
	static styles? = css`
		h2 {
			padding: 0 1rem;
			display: flex;
			align-items: center;
			justify-content: center;
			svg {
				margin-right: 0.5rem;
			}
		}
	`

	// /** 本地化控制器 */
	// private _localize = new LocalizeController(this)

	@property()
	title = &apos;Hyosan Chat&apos;
	render() {
		return html`
      &amp;lt;header&amp;gt;
        &amp;lt;h2&amp;gt;
					&amp;lt;svg t=&quot;1740983223876&quot; class=&quot;icon&quot; viewBox=&quot;0 0 1024 1024&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; p-id=&quot;3643&quot; width=&quot;2rem&quot; height=&quot;2rem&quot;&amp;gt;&amp;lt;path d=&quot;M853.333333 85.333333H170.666667C123.52 85.333333 85.76 123.52 85.76 170.666667L85.333333 938.666667l170.666667-170.666667h597.333333c47.146667 0 85.333333-38.186667 85.333334-85.333333V170.666667c0-47.146667-38.186667-85.333333-85.333334-85.333334zM256 384h512v85.333333H256v-85.333333z m341.333333 213.333333H256v-85.333333h341.333333v85.333333z m170.666667-256H256v-85.333333h512v85.333333z&quot; p-id=&quot;3644&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;
					&amp;lt;span&amp;gt;
						${this.title}
					&amp;lt;/span&amp;gt;
				&amp;lt;/h2&amp;gt;
      &amp;lt;/header&amp;gt;
    `
	}
}

declare global {
	interface HTMLElementTagNameMap {
		&apos;hyosan-chat-conversations-header&apos;: HyosanChatConversationsHeader
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/hyosan-chat-conversations-item.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ShoelaceElement from &apos;@/internal/shoelace-element&apos;
import type { Conversation } from &apos;@/types/conversations&apos;
// import { LocalizeController } from &apos;@shoelace-style/localize&apos;
import { css, html } from &apos;lit&apos;
import { customElement, property } from &apos;lit/decorators.js&apos;

/** 会话列表项 组件 */
@customElement(&apos;hyosan-chat-conversations-item&apos;)
export class HyosanChatConversationsItem extends ShoelaceElement {
	static styles? = css`
    .item-row { padding: 0.5rem; margin: 0.5rem; border-radius: 0.5rem; cursor: pointer; }
		:host([actived]) .item-row, .item-row:hover { background-color: var(--sl-color-neutral-200); }
  `

	// /** 本地化控制器 */
	// private _localize = new LocalizeController(this)

	/** 是否选中 */
	@property({ type: Boolean })
	actived = false

	/** 会话列表数据源 */
	@property({ attribute: false, type: Object })
	item!: Conversation

	render() {
		return html`
      &amp;lt;div class=&quot;item-row&quot; @click=${() =&amp;gt; this.emit(&apos;click-conversation&apos;, { detail: { item: this.item } })}&amp;gt;
        &amp;lt;span&amp;gt;${this.item.label}&amp;lt;/span&amp;gt;
      &amp;lt;/div&amp;gt;
    `
	}
}

declare global {
	interface HTMLElementTagNameMap {
		&apos;hyosan-chat-conversations-item&apos;: HyosanChatConversationsItem
	}
	interface GlobalEventHandlersEventMap {
		&apos;click-conversation&apos;: CustomEvent&amp;lt;{ item: Conversation }&amp;gt;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/hyosan-chat-conversations.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import ShoelaceElement from &apos;@/internal/shoelace-element&apos;
import { HasSlotController } from &apos;@/internal/slot&apos;
import type { Conversation } from &apos;@/types/conversations&apos;
// import { LocalizeController } from &apos;@shoelace-style/localize&apos;
import { css, html } from &apos;lit&apos;
import { customElement, property } from &apos;lit/decorators.js&apos;

/** 管理会话 组件 */
@customElement(&apos;hyosan-chat-conversations&apos;)
export class HyosanChatConversations extends ShoelaceElement {
	static styles? = css`
		:host { height: 100%; display: block; }
		.aside {
			display: flex;
			flex-direction: column;
			height: 100%;
			main {
				flex: 1;
				overflow-y: auto;
			}
		}
	`

	// /** 本地化控制器 */
	// private _localize = new LocalizeController(this)
	private readonly hasSlotController = new HasSlotController(
		this,
		&apos;conversations-header&apos;,
		&apos;conversations-footer&apos;,
	)

	/** 当前选中的值 */
	@property({ reflect: true })
	activeKey = &apos;&apos;

	/** 会话列表数据源 */
	@property({ attribute: false, type: Array })
	items: Conversation[] = []

	private _handleClickConversation(
		event: GlobalEventHandlersEventMap[&apos;click-conversation&apos;],
	) {
		this.activeKey = event.detail.item.key
		this.requestUpdate()
	}

	render() {
		return html`
			&amp;lt;div class=&quot;aside&quot;&amp;gt;
				&amp;lt;header&amp;gt;
					&amp;lt;slot name=&quot;conversations-header&quot;&amp;gt;&amp;lt;/slot&amp;gt;
				&amp;lt;/header&amp;gt;
				&amp;lt;main&amp;gt;
					${this.items.map(
						(item) =&amp;gt; html`
						&amp;lt;hyosan-chat-conversations-item
							.item=${item} ?actived=${this.activeKey === item.key}
							@click-conversation=${this._handleClickConversation}
						&amp;gt;
						&amp;lt;/hyosan-chat-conversations-item&amp;gt;
						`,
					)}
				&amp;lt;/main&amp;gt;
				&amp;lt;footer&amp;gt;
					&amp;lt;slot name=&quot;conversations-footer&quot;&amp;gt;&amp;lt;/slot&amp;gt;
				&amp;lt;/footer&amp;gt;
			&amp;lt;/div&amp;gt;
		`
	}
}

declare global {
	interface HTMLElementTagNameMap {
		&apos;hyosan-chat-conversations&apos;: HyosanChatConversations
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/index.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export { HyosanChat } from &apos;./hyosan-chat&apos;
export { HyosanChatConversations } from &apos;./hyosan-chat-conversations&apos;
export { HyosanChatConversationsItem } from &apos;./hyosan-chat-conversations-item&apos;
export { HyosanChatConversationsHeader } from &apos;./hyosan-chat-conversations-header&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;事件&lt;/h2&gt;
&lt;p&gt;由于 &lt;code&gt;Lit&lt;/code&gt; 的事件就是原生 &lt;code&gt;HTML&lt;/code&gt; 事件, 没有任何类型约束, 所以在 &lt;code&gt;hyosan-chat-conversations-item&lt;/code&gt; 组件中, 添加了 &lt;code&gt;click-conversation&lt;/code&gt; 事件, 用于通知父组件选中的会话; 其中 &lt;code&gt;this.emit&lt;/code&gt; 是在 &lt;code&gt;ShoelaceElement&lt;/code&gt; 中定义的, 我们将 &lt;code&gt;shoelace&lt;/code&gt; 中的相关代码复制到 &lt;code&gt;src/internal/shoelace-element.ts&lt;/code&gt; 中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { LitElement } from &apos;lit&apos;
import { property } from &apos;lit/decorators.js&apos;

/**
 * 组件基础类, 参考自 shoelace
 * @see https://github.com/shoelace-style/shoelace/blob/6f09a7556731107e027b8afade0ad1e28d77c710/src/internal/shoelace-element.ts#L65
 */
export default class ShoelaceElement extends LitElement {
	// Make localization attributes reactive
	@property() dir = &apos;ltr&apos;
	@property() lang = &apos;&apos;

	/** Emits a custom event with more convenient defaults. */
	emit&amp;lt;T extends string &amp;amp; keyof EventTypesWithoutRequiredDetail&amp;gt;(
		name: EventTypeDoesNotRequireDetail&amp;lt;T&amp;gt;,
		options?: SlEventInit&amp;lt;T&amp;gt; | undefined,
	): GetCustomEventType&amp;lt;T&amp;gt;
	emit&amp;lt;T extends string &amp;amp; keyof EventTypesWithRequiredDetail&amp;gt;(
		name: EventTypeRequiresDetail&amp;lt;T&amp;gt;,
		options: SlEventInit&amp;lt;T&amp;gt;,
	): GetCustomEventType&amp;lt;T&amp;gt;
	emit&amp;lt;T extends string &amp;amp; keyof ValidEventTypeMap&amp;gt;(
		name: T,
		options?: SlEventInit&amp;lt;T&amp;gt; | undefined,
	): GetCustomEventType&amp;lt;T&amp;gt; {
		const event = new CustomEvent(name, {
			bubbles: true,
			cancelable: false,
			composed: true,
			detail: {},
			...options,
		})

		this.dispatchEvent(event)

		return event as GetCustomEventType&amp;lt;T&amp;gt;
	}
}

/** Match event type name strings that are registered on GlobalEventHandlersEventMap... */
type EventTypeRequiresDetail&amp;lt;T&amp;gt; = T extends keyof GlobalEventHandlersEventMap
	? // ...where the event detail is an object...
		GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;
			Record&amp;lt;PropertyKey, unknown&amp;gt;
		&amp;gt;
		? // ...that is non-empty...
			GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;
				Record&amp;lt;PropertyKey, never&amp;gt;
			&amp;gt;
			? never
			: // ...and has at least one non-optional property
				Partial&amp;lt;
						GlobalEventHandlersEventMap[T][&apos;detail&apos;]
					&amp;gt; extends GlobalEventHandlersEventMap[T][&apos;detail&apos;]
				? never
				: T
		: never
	: never

/** The inverse of the above (match any type that doesn&apos;t match EventTypeRequiresDetail) */
type EventTypeDoesNotRequireDetail&amp;lt;T&amp;gt; =
	T extends keyof GlobalEventHandlersEventMap
		? GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;
				Record&amp;lt;PropertyKey, unknown&amp;gt;
			&amp;gt;
			? GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;
					Record&amp;lt;PropertyKey, never&amp;gt;
				&amp;gt;
				? T
				: Partial&amp;lt;
							GlobalEventHandlersEventMap[T][&apos;detail&apos;]
						&amp;gt; extends GlobalEventHandlersEventMap[T][&apos;detail&apos;]
					? T
					: never
			: T
		: T

/** `keyof EventTypesWithRequiredDetail` lists all registered event types that require detail */
type EventTypesWithRequiredDetail = {
	[EventType in keyof GlobalEventHandlersEventMap as EventTypeRequiresDetail&amp;lt;EventType&amp;gt;]: true
}

/** `keyof EventTypesWithoutRequiredDetail` lists all registered event types that do NOT require detail */
type EventTypesWithoutRequiredDetail = {
	[EventType in keyof GlobalEventHandlersEventMap as EventTypeDoesNotRequireDetail&amp;lt;EventType&amp;gt;]: true
}

/** Helper to make a specific property of an object non-optional  */
type WithRequired&amp;lt;T, K extends keyof T&amp;gt; = T &amp;amp; { [P in K]-?: T[P] }

/**
 * Given an event name string, get a valid type for the options to initialize the event that is more restrictive than
 * just CustomEventInit when appropriate (validate the type of the event detail, and require it to be provided if the
 * event requires it)
 */
type SlEventInit&amp;lt;T&amp;gt; = T extends keyof GlobalEventHandlersEventMap
	? GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;
			Record&amp;lt;PropertyKey, unknown&amp;gt;
		&amp;gt;
		? GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;
				Record&amp;lt;PropertyKey, never&amp;gt;
			&amp;gt;
			? CustomEventInit&amp;lt;GlobalEventHandlersEventMap[T][&apos;detail&apos;]&amp;gt;
			: Partial&amp;lt;
						GlobalEventHandlersEventMap[T][&apos;detail&apos;]
					&amp;gt; extends GlobalEventHandlersEventMap[T][&apos;detail&apos;]
				? CustomEventInit&amp;lt;GlobalEventHandlersEventMap[T][&apos;detail&apos;]&amp;gt;
				: WithRequired&amp;lt;
						CustomEventInit&amp;lt;GlobalEventHandlersEventMap[T][&apos;detail&apos;]&amp;gt;,
						&apos;detail&apos;
					&amp;gt;
		: CustomEventInit
	: CustomEventInit

/** Given an event name string, get the type of the event */
type GetCustomEventType&amp;lt;T&amp;gt; = T extends keyof GlobalEventHandlersEventMap
	? GlobalEventHandlersEventMap[T] extends CustomEvent&amp;lt;unknown&amp;gt;
		? GlobalEventHandlersEventMap[T]
		: CustomEvent&amp;lt;unknown&amp;gt;
	: CustomEvent&amp;lt;unknown&amp;gt;

/** `keyof ValidEventTypeMap` is equivalent to `keyof GlobalEventHandlersEventMap` but gives a nicer error message */
type ValidEventTypeMap =
	| EventTypesWithRequiredDetail
	| EventTypesWithoutRequiredDetail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;emit&lt;/code&gt; 和类型体操完美实现了事件的类型声明和类型约束, 我们只需通过扩展 &lt;code&gt;GlobalEventHandlersEventMap&lt;/code&gt; 类型即可实现类型声明:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare global {
	interface GlobalEventHandlersEventMap {
		&apos;click-conversation&apos;: CustomEvent&amp;lt;{ item: Conversation }&amp;gt;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整改动&lt;/h2&gt;
&lt;p&gt;文中可能有遗漏的代码, 可直接参考:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/SublimeCT/hyosan-chat/commit/47b3acd5864b0186111f5162c3e303a695460c4f&quot;&gt;#47b3a&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最后让我们看一下效果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/hyosan-chat-conversations-base-demo.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://x.ant.design/index-cn&quot;&gt;ant-design-x&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev/docs/composition/controllers/&quot;&gt;Reactive Controllers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Lit 创建一个 AI 对话组件库 04 国际化 篇</title><link>http://blog.xiaban.run/posts/2025/hyosan-chat-04-i18n/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/hyosan-chat-04-i18n/</guid><description>当我们将组件库公开发布后, 可以被任何国家任何人使用, 我们除了要有完善可靠的功能, 还要考虑诸如 国际化 / ts 类型定义 / 主题 等可以提升用户体验的东西</description><pubDate>Sun, 02 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当我们将组件库公开发布后, 可以被任何国家任何人使用, 我们除了要有完善可靠的功能, 还要考虑诸如 国际化 / &lt;code&gt;ts&lt;/code&gt; 类型定义 / 主题 等可以提升用户体验的东西&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-01-create/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 01 搭建篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-02-prompts/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 02 Prompts 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-03-feasibility/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 03 可行性验证 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-04-i18n/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 04 国际化 篇&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;../hyosan-chat-03-feasibility/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 03 可行性验证 篇&lt;/a&gt; 中我们发现了组件库存在以下问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺少 &lt;strong&gt;国际化&lt;/strong&gt; 功能&lt;/li&gt;
&lt;li&gt;在 vue 中缺少 &lt;strong&gt;类型定义&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;缺少 &lt;strong&gt;主题&lt;/strong&gt; 功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于一个组件库来说, 我们的组件可能会被不同的项目使用, 如果我们不考虑多语言, 那就极大地限制了不同语言环境下的用户的使用, 所以我们需要考虑国际化; 对于类型定义和主题切换来说也是同样的道理&lt;/p&gt;
&lt;h2&gt;国际化&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://shoelace.style/&quot;&gt;shoelace&lt;/a&gt; 本身提供了 &lt;a href=&quot;https://shoelace.style/getting-started/localization&quot;&gt;国际化支持&lt;/a&gt;, &lt;code&gt;shoelace&lt;/code&gt; 是一个基于 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 的优秀的 UI 组件库, 我们阅读一下 shoelace 的源码, 借鉴一下 shoelace 的国际化实现:&lt;/p&gt;
&lt;p&gt;我们从语言文件开始, 例如 &lt;a href=&quot;https://github.com/shoelace-style/shoelace/blob/next/src/translations/zh-cn.ts&quot;&gt;src/translations/zh-cn.ts&lt;/a&gt;, 文件中使用了 &lt;a href=&quot;https://github.com/shoelace-style/shoelace/blob/next/src/utilities/localize.ts&quot;&gt;src/utilities/localize.ts&lt;/a&gt; 中导出的 &lt;code&gt;registerTranslation&lt;/code&gt;, &lt;code&gt;registerTranslation&lt;/code&gt; 实际上来自于 &lt;a href=&quot;https://www.npmjs.com/package/@shoelace-style/localize&quot;&gt;@shoelace-style/localize&lt;/a&gt; 包, 原来 shoelace 将本地化功能拆分为了一个单独的包, 而且可以与 &lt;code&gt;lit&lt;/code&gt; 完美结合, 零依赖!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@shoelace-style/localize&lt;/code&gt; 的示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { LocalizeController, registerTranslation } from &apos;@shoelace-style/localize&apos;;

// Note: translations can also be lazy loaded (see &quot;Registering Translations&quot; below)
import en from &apos;../translations/en&apos;;
import es from &apos;../translations/es&apos;;

registerTranslation(en, es);

@customElement(&apos;my-element&apos;)
export class MyElement extends LitElement {
  private localize = new LocalizeController(this);

  @property() lang: string;

  render() {
    return html`
      &amp;lt;h1&amp;gt;${this.localize.term(&apos;hello_world&apos;)}&amp;lt;/h1&amp;gt;
    `;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们引入了 &lt;code&gt;@shoelace-style/localize&lt;/code&gt; 并注册了多语言文案 &lt;code&gt;en&lt;/code&gt; / &lt;code&gt;es&lt;/code&gt;, 接着我们在组件中声明了 &lt;code&gt;localize = new LocalizeController(this)&lt;/code&gt; 和 &lt;code&gt;@property() lang: string&lt;/code&gt;, 最后在 render 中使用 &lt;code&gt;this.localize.term(&apos;hello_world&apos;)&lt;/code&gt; 来获取文案, 他会在 &lt;code&gt;lang&lt;/code&gt; 属性变化的时候自动更新多语言文案内容&lt;/p&gt;
&lt;p&gt;这是一个优秀的设计, 我们仅需要修改 &lt;code&gt;html[lang]&lt;/code&gt; 或 组件的 &lt;code&gt;lang&lt;/code&gt; 属性, 组件就能根据新的语言进行渲染; 我们来猜一下 &lt;code&gt;@shoelace-style/localize&lt;/code&gt; 是如何实现的, 首先 &lt;code&gt;new LocalizeController(this)&lt;/code&gt; 时将当前组件类的示例传入了 &lt;code&gt;LocalizeController&lt;/code&gt;, 并在 &lt;code&gt;LocalizeController&lt;/code&gt; 内部监听 &lt;code&gt;lang&lt;/code&gt; 属性的变化, 并且调用组件示例的用于渲染 &lt;code&gt;DOM&lt;/code&gt; 的方法...&lt;/p&gt;
&lt;p&gt;接下来让我们阅读 &lt;code&gt;@shoelace-style/localize&lt;/code&gt; 源码, 查看具体是如何实现的:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shoelace-style/localize/blob/04e58c3687c1cd67fd4aba423ce4ad60b6b2c215/src/index.ts#L100C14-L100C32&quot;&gt;localize/src/index.ts&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export class LocalizeController&amp;lt;UserTranslation extends Translation = DefaultTranslation&amp;gt;
  implements ReactiveController
{
  constructor(host: ReactiveControllerHost &amp;amp; HTMLElement) {
    this.host = host;
    this.host.addController(this);
  }
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先在 &lt;code&gt;constructor&lt;/code&gt; 中调用了组件的 &lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#addController&quot;&gt;addController&lt;/a&gt;, 这是一个用于向 &lt;code&gt;LitElement&lt;/code&gt; 添加响应式控制器的方法, 它允许外部代码与 &lt;code&gt;Lit&lt;/code&gt; 的响应式生命周期集成, 简而言之 &lt;strong&gt;&lt;code&gt;LocalizeController&lt;/code&gt; 可以直接调用组件的生命周期 hooks, 也就可以直接更新组件&lt;/strong&gt;, 详见 &lt;a href=&quot;https://lit.dev/docs/composition/controllers/&quot;&gt;Reactive Controllers&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const connectedElements = new Set&amp;lt;HTMLElement&amp;gt;();
if (isClient) {
  const documentElementObserver = new MutationObserver(update);
  documentDirection = document.documentElement.dir || &apos;ltr&apos;;
  documentLanguage = document.documentElement.lang || navigator.language;

  // Watch for changes on &amp;lt;html lang&amp;gt;
  documentElementObserver.observe(document.documentElement, {
    attributes: true,
    attributeFilter: [&apos;dir&apos;, &apos;lang&apos;]
  });
}
/** Updates all localized elements that are currently connected */
export function update() {
  if (isClient) {
    documentDirection = document.documentElement.dir || &apos;ltr&apos;;
    documentLanguage = document.documentElement.lang || navigator.language;
  }

  [...connectedElements.keys()].map((el: LitElement) =&amp;gt; {
    if (typeof el.requestUpdate === &apos;function&apos;) {
      el.requestUpdate();
    }
  });
}
// ...

export class LocalizeController&amp;lt;UserTranslation extends Translation = DefaultTranslation&amp;gt;
  implements ReactiveController
{
  hostConnected() {
    connectedElements.add(this.host);
  }

  hostDisconnected() {
    connectedElements.delete(this.host);
  }
  /**
   * Gets the host element&apos;s language as determined by the `lang` attribute. The return value is transformed to
   * lowercase.
   */
  lang() {
    return `${this.host.lang || documentLanguage}`.toLowerCase();
  }
  /** Outputs a translated term. */
  term&amp;lt;K extends keyof UserTranslation&amp;gt;(key: K, ...args: FunctionParams&amp;lt;UserTranslation[K]&amp;gt;): string {
    const { primary, secondary } = this.getTranslationData(this.lang());
    // ...
  }
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里先把组件存到了 &lt;code&gt;connectedElements&lt;/code&gt; 中, 然后在外部通过 &lt;code&gt;MutationObserver&lt;/code&gt; 监听了 &lt;code&gt;HTML&lt;/code&gt; 标签的 &lt;code&gt;lang&lt;/code&gt; 属性变化, 并同步调用 &lt;code&gt;update&lt;/code&gt; 调用组件的 &lt;code&gt;requestUpdate()&lt;/code&gt; 来更新组件; 除此之外, 在组件内部的 &lt;code&gt;lang&lt;/code&gt; 属性变化时, 也会调用 &lt;code&gt;render&lt;/code&gt;, 并调用 &lt;code&gt;term&lt;/code&gt; 来根据最新的 &lt;code&gt;lang&lt;/code&gt; 更新组件&lt;/p&gt;
&lt;p&gt;至此 &lt;code&gt;@shoelace-style/localize&lt;/code&gt; 的示例的具体实现我们就分析完了, 接下来我们看一下 &lt;code&gt;shoelace&lt;/code&gt; 是如何使用 &lt;code&gt;@shoelace-style/localize&lt;/code&gt; 的:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shoelace-style/shoelace/blob/6f09a7556731107e027b8afade0ad1e28d77c710/src/internal/shoelace-element.ts#L65&quot;&gt;src/internal/shoelace-element.ts&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
export default class ShoelaceElement extends LitElement {
  // Make localization attributes reactive
  @property() dir: string;
  @property() lang: string;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;shoelace&lt;/code&gt; 并没有在每个组件上都声明 &lt;code&gt;lang&lt;/code&gt; / &lt;code&gt;dir&lt;/code&gt;, 而是创建了一个基类 &lt;code&gt;ShoelaceElement&lt;/code&gt;, 并在所有组件上继承了它, 这样 &lt;code&gt;shoelace&lt;/code&gt; 就可以统一处理 &lt;code&gt;lang&lt;/code&gt; / &lt;code&gt;dir&lt;/code&gt; 了, 不得不说这是一个很优秀的设计&lt;/p&gt;
&lt;p&gt;:::tip
实际上 &lt;code&gt;ShoelaceElement&lt;/code&gt; 类中还有很多值得学习的地方:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型安全的 &lt;code&gt;emit&lt;/code&gt; 方法, 通过 &lt;strong&gt;函数重载&lt;/strong&gt; 实现了 &lt;code&gt;emit&lt;/code&gt; 方法的三种不同参数类型, 并实现了很好的类型约束&lt;/li&gt;
&lt;li&gt;组件的动态注册, &lt;code&gt;define&lt;/code&gt; 方法实现了 &lt;strong&gt;动态注册&lt;/strong&gt; 和 &lt;strong&gt;重复注册检查&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态依赖加载&lt;/strong&gt;, 在 &lt;code&gt;constructor&lt;/code&gt; 中动态注册组件声明的 &lt;code&gt;dependencies&lt;/code&gt;, 详见 &lt;a href=&quot;https://shoelace.style/getting-started/installation#cherry-picking&quot;&gt;Cherry Pick - Shoelace&lt;/a&gt;
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;国际化实现与组件基类&lt;/h2&gt;
&lt;p&gt;回到我们的组件库项目中, 我们来基于 &lt;code&gt;@shoelace-style/localize&lt;/code&gt; 一步步实现本地化功能, 并且借鉴 &lt;code&gt;ShoelaceElement&lt;/code&gt; 的设计:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code src/internal/shoelace-element.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { LitElement } from &quot;lit&quot;;
import { property } from &quot;lit/decorators.js&quot;;

export default class ShoelaceElement extends LitElement {
  // Make localization attributes reactive
  @property() dir = &apos;ltr&apos;;
  @property() lang = &apos;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
+		&quot;baseUrl&quot;: &quot;./&quot;,
+		&quot;paths&quot;: {
+			&quot;@/*&quot;: [&quot;src/*&quot;]
+		},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/translations/zh-cn.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { registerTranslation, type Translation } from &apos;@/translations/translation&apos;;

const translation: Translation = {
  $code: &apos;zh-cn&apos;,
  $name: &apos;简体中文&apos;,
  $dir: &apos;ltr&apos;,

  test: &apos;测试&apos;,
};

registerTranslation(translation);

export default translation;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/translations/en.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { registerTranslation, type Translation } from &apos;@/translations/translation&apos;;

const translation: Translation = {
  $code: &apos;en&apos;,
  $name: &apos;English&apos;,
  $dir: &apos;ltr&apos;,

  test: &apos;test&apos;,
};

registerTranslation(translation);

export default translation;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/translations/translation.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Translation as DefaultTranslation } from &apos;@shoelace-style/localize&apos;;

// Export functions from the localize lib so we have one central place to import them from
export { registerTranslation } from &apos;@shoelace-style/localize&apos;;

export interface Translation extends DefaultTranslation {
  $code: string; // e.g. en, en-GB
  $name: string; // e.g. English, Español
  $dir: &apos;ltr&apos; | &apos;rtl&apos;;

  test: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/utils/localize.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { LocalizeController as DefaultLocalizationController, registerTranslation } from &apos;@shoelace-style/localize&apos;;
import type { Translation } from &apos;@/translations/translation&apos;;
import en from &apos;.@/translations/en&apos;; // Register English as the default/fallback language
import zhCn from &apos;@/translations/zh-cn&apos;; // Register English as the default/fallback language

/**
 * Extend the controller and apply our own translation interface for better typings
 * @see https://github.com/shoelace-style/shoelace/blob/next/src/utilities/localize.ts
 */
export class LocalizeController extends DefaultLocalizationController&amp;lt;Translation&amp;gt; {
  // Technicallly &apos;../translations/en.js&apos; is supposed to work via side-effects. However, by some mystery sometimes the
  // translations don&apos;t get bundled as expected resulting in `no translation found` errors.
  // This is basically some extra assurance that our translations get registered prior to our localizer connecting in a component
  // and we don&apos;t rely on implicit import ordering.
  static {
    registerTranslation(en);
    registerTranslation(zhCn);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/hyosan-chat.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ import ShoelaceElement from &apos;@/internal/shoelace-element&apos;
+ import { LocalizeController } from &apos;@/utils/localize&apos;
- export class HyosanChat extends LitElement {
+ export class HyosanChat extends ShoelaceElement {
+   private _locailze = new LocalizeController(this)
	render() {
		return html`
			&amp;lt;h2&amp;gt;${this.message}&amp;lt;/h2&amp;gt;
			&amp;lt;sl-button variant=&quot;primary&quot;&amp;gt;Hello Shoelace&amp;lt;/sl-button&amp;gt;
+			&amp;lt;p&amp;gt;${this._locailze.term(&apos;test&apos;)}&amp;lt;/p&amp;gt;
    `
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们创建了一些本地化相关的类和函数, 并在组件中通过 &lt;code&gt;LocalizeController&lt;/code&gt; 来实现本地化功能, 我们来测试一下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改组件的 &lt;code&gt;lang&lt;/code&gt; 属性:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/hyosan-chat-localize-attribute.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改 &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; 的 &lt;code&gt;lang&lt;/code&gt; 属性:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/hyosan-chat-localize-html-attribute.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://shoelace.style/&quot;&gt;shoelace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shoelace.style/getting-started/localization&quot;&gt;Localization - shoelace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev/docs/composition/controllers/&quot;&gt;Reactive Controllers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shoelace.style/getting-started/installation#cherry-picking&quot;&gt;Cherry Pick - Shoelace&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Lit 创建一个 AI 对话组件库 03 可行性验证 篇</title><link>http://blog.xiaban.run/posts/2025/hyosan-chat-03-feasibility/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/hyosan-chat-03-feasibility/</guid><description>本文将介绍组件库在打包发布后, 在实际的项目中使用是否可行，以及如何进行验证</description><pubDate>Sat, 01 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文将介绍组件库在打包发布后, 在实际的项目中使用是否可行，以及如何进行验证&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-01-create/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 01 搭建篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-02-prompts/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 02 Prompts 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-03-feasibility/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 03 可行性验证 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-04-i18n/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 04 国际化 篇&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;为什么要验证可行性&lt;/h2&gt;
&lt;p&gt;当我们打算使用一项新技术或新的库时, &lt;strong&gt;最先要做的就是验证可行性&lt;/strong&gt;; 因为对于新的外部依赖, 我们无法确定是否符合预期, 以及是否会带来新的问题, 进行一次最小规模的验证, 可以 &lt;strong&gt;尽早的暴露问题&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;打包&lt;/h2&gt;
&lt;p&gt;我们的项目是使用 &lt;a href=&quot;https://cn.vitejs.dev/guide/&quot;&gt;vite&lt;/a&gt; 进行搭建的, &lt;code&gt;vite&lt;/code&gt; 提供了 &lt;a href=&quot;https://cn.vitejs.dev/guide/build.html#library-mode&quot;&gt;library mode&lt;/a&gt; 以支持将项目中的部分文件进行单独打包, 并构建为库以供其他项目使用&lt;/p&gt;
&lt;h3&gt;TypeScript 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 引入生成 .d.ts 文件的插件
pnpm i -D vite-plugin-dts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;compilerOptions&quot;: {
    // ...
+		&quot;declaration&quot;: true,
+		&quot;outDir&quot;: &quot;dist&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;src/lib.ts&lt;/code&gt; 作为组件库的入口文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export * from &apos;./components&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的项目是使用 &lt;code&gt;ts&lt;/code&gt; 编写的, 为了让我们的组件库可以支持 &lt;code&gt;ts&lt;/code&gt; 项目, 我们需要使用 &lt;a href=&quot;https://github.com/qmhc/vite-plugin-dts/blob/HEAD/README.zh-CN.md&quot;&gt;vite-plugin-dts&lt;/a&gt; 将 &lt;code&gt;.d.ts&lt;/code&gt; 文件生成到 &lt;code&gt;dist&lt;/code&gt; 目录下&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vite.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ import dts from &apos;vite-plugin-dts&apos;

export default defineConfig({
  // ...
+ 	plugins: [
+ 		// 用于生成 `d.ts` 文件, refer https://github.com/qmhc/vite-plugin-dts/blob/HEAD/README.zh-CN.md
+ 		dts({ tsconfigPath: &apos;./tsconfig.lib.json&apos; })
+ 	],
+ 	build: {
+ 		lib: {
+ 			name: &apos;hyosan-chat&apos;,
+ 			entry: [&apos;src/lib.ts&apos;],
+ 			fileName: &apos;hyosan-chat&apos;,
+ 		},
+ 	},
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;tsconfig.lib.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;extends&quot;: &quot;./tsconfig.json&quot;, // 继承已有的 tsconfig.json
  &quot;include&quot;: [ // 添加需要在打包时生产 .d.ts 文件的文件
    &quot;./src/lib.ts&quot;,
    &quot;./src/components/**/*.ts&quot;,
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
为什么要额外创建一个 &lt;code&gt;tsconfig.json&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;在打包时, &lt;strong&gt;&lt;code&gt;vite-plugin-dts&lt;/code&gt; 会将 &lt;code&gt;tsconfig.json&lt;/code&gt; 中的 &lt;code&gt;include&lt;/code&gt; 中的文件进行打包(这里指生成 &lt;code&gt;dts&lt;/code&gt;)&lt;/strong&gt;, 但 &lt;code&gt;tsconfig.json&lt;/code&gt; 中 &lt;code&gt;include&lt;/code&gt; 包含了项目中的所有文件, 也包括了不应该出现在 &lt;code&gt;dist&lt;/code&gt; 目录中的文件, &lt;strong&gt;但我们不能修改 &lt;code&gt;tsconfig.json&lt;/code&gt;, 因为它适用于整个项目的开发环境&lt;/strong&gt;, 所以我们需要创建单独的 &lt;code&gt;tsconfig.lib.json&lt;/code&gt; 用于生成 &lt;code&gt;d.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也就是说, &lt;strong&gt;&lt;code&gt;tsconfig.json&lt;/code&gt; 用于开发环境, &lt;code&gt;tsconfig.lib.json&lt;/code&gt; 用于打包时生成 &lt;code&gt;d.ts&lt;/code&gt;&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们将 &lt;code&gt;src/lib.ts&lt;/code&gt; 中导出的组件作为整个组件库对外提供的组件&lt;/strong&gt;, 并通过 &lt;a href=&quot;https://github.com/qmhc/vite-plugin-dts/blob/HEAD/README.zh-CN.md&quot;&gt;vite-plugin-dts&lt;/a&gt; 生成了类型描述文件&lt;/p&gt;
&lt;h3&gt;package.json 配置&lt;/h3&gt;
&lt;p&gt;当我们的项目作为组件库供外部使用时, 需要在 &lt;code&gt;package.json&lt;/code&gt; 中声明 &lt;strong&gt;组件库的基本信息和对外提供的文件&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
+	 &quot;description&quot;: &quot;A library of web components for AI conversations based on lit and shoelace&quot;,
+	 &quot;keywords&quot;: [&quot;hyosan-chat&quot;, &quot;Lit&quot;, &quot;Shoelace&quot;, &quot;AI&quot;, &quot;chat&quot;, &quot;Web Components&quot;, &quot;web chat&quot;],
+	 &quot;author&quot;: {
+	  	&quot;name&quot;: &quot;Ryan&quot;
+	 },
+	 &quot;license&quot;: &quot;MIT&quot;,
-	 &quot;version&quot;: &quot;0.0.0&quot;,
+	 &quot;version&quot;: &quot;0.0.1&quot;,
-  &quot;private&quot;: true,
+ 	&quot;files&quot;: [
+ 		&quot;dist&quot;
+ 	],
+ 	&quot;main&quot;: &quot;./dist/hyosan-chat.umd.cjs&quot;,
+ 	&quot;module&quot;: &quot;./dist/hyosan-chat.js&quot;,
+ 	&quot;exports&quot;: {
+ 		&quot;.&quot;: {
+ 			&quot;import&quot;: &quot;./dist/hyosan-chat.js&quot;,
+ 			&quot;require&quot;: &quot;./dist/hyosan-chat.umd.cjs&quot;
+ 		}
+ 	},
	&quot;scripts&quot;: {
+		&quot;build:pack&quot;: &quot;pnpm run build &amp;amp;&amp;amp; pnpm pack&quot;,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;build &amp;amp; pack&lt;/h3&gt;
&lt;p&gt;修改完打包所需的配置后, 我们就可以使用 &lt;code&gt;pnpm run build&lt;/code&gt; 进行打包了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 打包
pnpm run build

# 打包并生成 .tgz 文件
pnpm run build:pack
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;lsd --tree dist/
 dist
├──  components
│   ├──  hyosan-chat.d.ts
│   └──  index.d.ts
├──  hyosan-chat.js
├──  hyosan-chat.umd.cjs
├──  lib.d.ts
└──  vite.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们观察一下打包后的产物:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;components/hyosan-chat.d.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对话组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;components/index.d.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;组件库中包含的组件的入口文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hyosan-chat.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;es&lt;/code&gt; 打包格式产物&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hyosan-chat.umd.cjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;umd&lt;/code&gt; 打包格式产物&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lib.d.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;组件库的入口文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vite.svg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vite logo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;其中 &lt;code&gt;vite.svg&lt;/code&gt; 不应该出现在 &lt;code&gt;dist&lt;/code&gt; 目录中, 因此我们将其删除&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;rm public/vite.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lib.d.ts&lt;/code&gt; 是组件库的入口文件的类型描述文件, 我们需要在 &lt;code&gt;package.json&lt;/code&gt; 中声明:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
+  &quot;types&quot;: &quot;./dist/lib.d.ts&quot;
	&quot;exports&quot;: {
		&quot;.&quot;: {
+ 			&quot;types&quot;: &quot;./dist/lib.d.ts&quot;,
			&quot;import&quot;: &quot;./dist/hyosan-chat.js&quot;,
			&quot;require&quot;: &quot;./dist/hyosan-chat.umd.cjs&quot;
		}
	},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dist&lt;/code&gt; / &lt;code&gt;hyosan-chat-0.0.1.tgz&lt;/code&gt; 文件作为打包后的产物, 不应该提交到仓库中, 我们将其添加到 &lt;code&gt;.gitignore&lt;/code&gt; 中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+dist
+hyosan-chat-*.tgz
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证&lt;/h2&gt;
&lt;p&gt;打包完成后, 我们需要创建一个新的 &lt;code&gt;vue&lt;/code&gt; 项目来引入组件库进行测试:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm create vite
.../1954f95e4b3-1078                     |   +1 +
.../1954f95e4b3-1078                     | Progress: resolved 1, reused 0, downloaded 1, added 1, done
│
◇  Project name:
│  vite-project
│
◇  Select a framework:
│  Vue
│
◇  Select a variant:
│  TypeScript
│
◇  Scaffolding project in /Users/xxx/projects/vite-project...
│
└  Done. Now run:

  cd vite-project
  pnpm install
  pnpm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cd vite-project
pnpm i &amp;amp;&amp;amp; pnpm i ./hyosan-chat/hyosan-chat-0.0.1.tgz
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用 &lt;code&gt;vscode&lt;/code&gt; 打开项目, 检查有无 报错 / 引入失败 / 类型缺失 等问题:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-project-import-hyosan-chat.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;vscode&lt;/code&gt; 中打开 &lt;code&gt;HelloWorrld.vue&lt;/code&gt; 尝试引入组件库并添加 &lt;code&gt;&amp;lt;hyosan-chat&amp;gt;&lt;/code&gt; 组件, 发现组件可以成功引入, 也有正确的类型定义&lt;/p&gt;
&lt;p&gt;下面我们启动项目查看页面显示是否正常:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-project-import-hyosan-chat-element-error.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;页面没有显示出组件, 这里的报错(&lt;code&gt;Class constructor Y cannot be invoked without &apos;new&apos;&lt;/code&gt;)是因为 &lt;code&gt;vue&lt;/code&gt; 把它当做了 &lt;code&gt;vue&lt;/code&gt; 组件进行了渲染, 但其实它应该被当做 &lt;strong&gt;自定义元素&lt;/strong&gt;, 通过查看 &lt;a href=&quot;https://cn.vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue&quot;&gt;在 vue 中使用自定义元素&lt;/a&gt;, 发现应该在 &lt;code&gt;vite.config.ts&lt;/code&gt; 中添加 &lt;code&gt;isCustomElement&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import vue from &apos;@vitejs/plugin-vue&apos;

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: tag =&amp;gt; tag.includes(&apos;hyosan-&apos;)
        }
      }
    })
  ],
  server: {
    port: 35195,
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-project-import-hyosan-chat-missing-sheets.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加 &lt;code&gt;isCustomElement&lt;/code&gt; 后, 组件渲染出来了, 但是样式没有, 这是因为我们在搭建项目引入 &lt;code&gt;shoelace&lt;/code&gt; 时, 使用最简单的 &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; &lt;code&gt;cdn URL&lt;/code&gt; 的方式引入了 &lt;code&gt;shoelace&lt;/code&gt; 的样式, 在新创建的项目中并没有这个 &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, 我们为了进行验证, 先引入 &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, 在后续文章中我们会优化引入方式&lt;/p&gt;
&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+    &amp;lt;link
+      rel=&quot;stylesheet&quot;
+      media=&quot;(prefers-color-scheme:light)&quot;
+      href=&quot;https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.0/cdn/themes/light.css&quot;
+    /&amp;gt;
+    &amp;lt;link
+      rel=&quot;stylesheet&quot;
+      media=&quot;(prefers-color-scheme:dark)&quot;
+      href=&quot;https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.0/cdn/themes/dark.css&quot;
+      onload=&quot;document.documentElement.classList.add(&apos;sl-theme-dark&apos;);&quot;
+    /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-project-import-hyosan-chat-success.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再次查看页面, 组件正常渲染, 接下来我们测试一下 &lt;code&gt;v-model&lt;/code&gt; 是否可用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+  &amp;lt;input v-model=&quot;message&quot; /&amp;gt;
-  &amp;lt;hyosan-chat&amp;gt;&amp;lt;/hyosan-chat&amp;gt;
+  &amp;lt;hyosan-chat :message=&quot;message&quot;&amp;gt;&amp;lt;/hyosan-chat&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;+ const message = ref(&apos;message&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-project-import-hyosan-chat-v-model.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;input&lt;/code&gt; 中修改 &lt;code&gt;message&lt;/code&gt;, 页面中的 &lt;code&gt;hyosan-chat&lt;/code&gt; 组件也会同步修改&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;看起来一切正常! 🎉 但在我们现在依然有几个需要解决的问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组件的样式现在依然使用的是 &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; &lt;code&gt;cdn&lt;/code&gt; 的方式实现, 但我们的用户使用环境可能是内网, 无法访问 &lt;code&gt;cdn&lt;/code&gt; 的资源, 因此我们需要优化引入方式, 改为 &lt;strong&gt;在组件库中 &lt;code&gt;export&lt;/code&gt; &lt;code&gt;shoelace&lt;/code&gt; 的样式&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vscode&lt;/code&gt; 中的 &lt;code&gt;HelloWorld.vue&lt;/code&gt; 中编写 &lt;code&gt;&amp;lt;hyosan-chat&amp;gt;&lt;/code&gt; 时, 没有关于 &lt;code&gt;hyosan-chat&lt;/code&gt; 组件的类型提示(&lt;code&gt;props&lt;/code&gt; / &lt;code&gt;events&lt;/code&gt;), 也就是 &lt;code&gt;vscode&lt;/code&gt; 无法识别 &lt;code&gt;hyosan-chat&lt;/code&gt; 组件的类型; 得益于 &lt;code&gt;vue&lt;/code&gt; 对自定义组件的支持, 我们可以 &lt;strong&gt;生成类型定义文件对 &lt;code&gt;vue GlobalComponents&lt;/code&gt; 进行扩展&lt;/strong&gt;, 这里我们可以使用一些第三方库(&lt;a href=&quot;https://www.npmjs.com/package/custom-element-vuejs-integration&quot;&gt;custom-element-vuejs-integration&lt;/a&gt;) 来实现, 详见 &lt;a href=&quot;https://cn.vuejs.org/guide/extras/web-components.html#non-vue-web-components-and-typescript&quot;&gt;非 Vue Web Components 和 TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;组件库没有 &lt;strong&gt;提供多语言支持&lt;/strong&gt;, 虽然 &lt;code&gt;shoelace&lt;/code&gt; 提供了 &lt;a href=&quot;https://shoelace.style/getting-started/localization&quot;&gt;本地化&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/shoelace-style/shoelace/tree/current/src/translations&quot;&gt;语言包文件&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题我们将在后续章节中一一解决&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.vitejs.dev/guide/build.html#library-mode&quot;&gt;库模式 - vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/qmhc/vite-plugin-dts/blob/HEAD/README.zh-CN.md&quot;&gt;vite-plugin-dts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue&quot;&gt;在 vue 中使用自定义元素&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.vuejs.org/guide/extras/web-components.html#non-vue-web-components-and-typescript&quot;&gt;非 Vue Web Components 和 TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/custom-element-vuejs-integration&quot;&gt;custom-element-vuejs-integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Lit 创建一个 AI 对话组件库 02 Prompts 篇</title><link>http://blog.xiaban.run/posts/2025/hyosan-chat-02-prompts/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/hyosan-chat-02-prompts/</guid><description>本文将介绍如何构建开发过程中与 AI 对话所需的 prompts</description><pubDate>Thu, 27 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文将深入探讨如何通过结构化提示工程(&lt;code&gt;Prompt Engineering&lt;/code&gt;) 帮助 &lt;code&gt;AI&lt;/code&gt; 深度理解项目上下文, 从而提升编码辅助效率&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-01-create/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 01 搭建篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-02-prompts/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 02 Prompts 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-03-feasibility/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 03 可行性验证 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-04-i18n/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 04 国际化 篇&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;随着 &lt;code&gt;LLM&lt;/code&gt; 技术的发展, &lt;code&gt;AI&lt;/code&gt; 的编码能力和代码阅读能力越来越强, 要实现真正的 &lt;code&gt;AGI&lt;/code&gt;, 也就是 &lt;code&gt;AI&lt;/code&gt; 替代人类(当然也包括程序员), 似乎最大的障碍只剩下了人类参差不齐的迷惑的表达能力, 也就是讲清楚自己要做什么, 并且让 AI 能够准确理解自己的需求; &lt;em&gt;听起来怎么这么像产品经理给程序员讲需求, 只是这次程序员变成了产品经理 😅&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;AI 取代论&lt;/h2&gt;
&lt;p&gt;最近爆火的 &lt;a href=&quot;https://www.deepseek.com/&quot;&gt;Deepseek&lt;/a&gt; 让我们感受到了 &lt;code&gt;AI&lt;/code&gt; 进化的速度似乎比我们想象的更快, 那 &lt;code&gt;AI&lt;/code&gt; 能够在何种程度上取代程序员呢? 让我们思考一下软件开发流程:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;产品经理提出需求&lt;/li&gt;
&lt;li&gt;评估需求可行性&lt;/li&gt;
&lt;li&gt;理解需求并编写设计文档&lt;/li&gt;
&lt;li&gt;编码&lt;/li&gt;
&lt;li&gt;测试&lt;/li&gt;
&lt;li&gt;修复 &lt;code&gt;bug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;上线&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过梳理开发流程可以看到, 真正用来编码的时间估计只有 &lt;code&gt;30% ~ 50%&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;1&lt;/code&gt; &lt;code&gt;2&lt;/code&gt; 中, 产品经理是真正的人类思维, 并且 &lt;strong&gt;人类的表达能力并不能保证其他人能够完全准确的理解需求&lt;/strong&gt;, 也并不懂某个需求在编码时能否实现, 更无法预知编码时需要考虑的东西, 所以就需要 &lt;strong&gt;需求评审&lt;/strong&gt; 会, 作为程序员经常要跟产品经理 &lt;code&gt;battle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3&lt;/code&gt; 系统设计, 程序员要 &lt;strong&gt;将人类语言拆解为可以用程序实现的具体的任务&lt;/strong&gt;, 把看起来笼统的需求具象化&lt;/li&gt;
&lt;li&gt;然后才开始第 &lt;code&gt;4&lt;/code&gt; 步编码, 除了实现需求外, 还要让编码 &lt;strong&gt;符合项目规范&lt;/strong&gt;, &lt;strong&gt;保持良好的代码风格&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;随后测试环节更不能掉以轻心, 因为测试是项目上线前的最后一环, 测试人员需要 &lt;strong&gt;将笼统的需求转换为可实际操作的测试用例&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;然后开始 &lt;code&gt;bug&lt;/code&gt; 消除环节, &lt;code&gt;bug&lt;/code&gt; 消灭之后上线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由此我们发现, 软件开发流程中真正的复杂性在于:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需求的整理和表达能力&lt;/li&gt;
&lt;li&gt;将人类语言转化为 软件系统设计文档&lt;/li&gt;
&lt;li&gt;将人类语言转化为 测试用例&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也正是现阶段 &lt;code&gt;AI&lt;/code&gt; 与屏幕前的用户之间的无形障碍, 就是人类需要 &lt;strong&gt;信息表达能力&lt;/strong&gt; 和 &lt;strong&gt;信息转换的能力&lt;/strong&gt;; 想象一下身边的大佬同事, 我们对他的评价时常是 &lt;strong&gt;他理解能力很强&lt;/strong&gt; / &lt;strong&gt;他表达能力很强, 让他给我们讲讲&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;解药&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Prompts&lt;/code&gt; 是 &lt;code&gt;AGI&lt;/code&gt; 时代的 编程语言&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;作为程序员如何能够更好的使用 &lt;code&gt;AI&lt;/code&gt; 呢? 在我看来就是编写 &lt;strong&gt;尽可能准确和详细的各种文档&lt;/strong&gt;, 在整个项目的视角上, 具体而言就是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt;: 描述整个项目&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CHANGELOGS.md&lt;/code&gt;: 描述版本更新信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CONTRIBUTING.md&lt;/code&gt;: 描述如何参与项目&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来我们创建这些文件&lt;/p&gt;
&lt;h2&gt;Prompts&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关键字&lt;/th&gt;
&lt;th&gt;构成说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;角色&lt;/td&gt;
&lt;td&gt;给 AI 定义一个最匹配任务的角色，比如：「你是一位软件工程师」「你是一位小学老师」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;指示&lt;/td&gt;
&lt;td&gt;对任务进行描述&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上下文&lt;/td&gt;
&lt;td&gt;给出与任务相关的其它背景信息（尤其在多轮交互中）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;例子&lt;/td&gt;
&lt;td&gt;必要时给出举例, 学术中称为 &lt;code&gt;one-shot learning&lt;/code&gt;, &lt;code&gt;few-shot learning&lt;/code&gt; 或 &lt;code&gt;in-context learning&lt;/code&gt;; 实践证明其对输出正确性有很大帮助&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输入&lt;/td&gt;
&lt;td&gt;任务的输入信息；在提示词中明确的标识出输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出&lt;/td&gt;
&lt;td&gt;输出的格式描述, 以便后继模块自动解析模型的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出&lt;/td&gt;
&lt;td&gt;结果, 比如(&lt;code&gt;JSON / XML&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;使用 repomix 提取项目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm i -g repomix
repomix --init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;repomix&lt;/code&gt; 会生成以下几个文件:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;repomix.config.json&lt;/code&gt;: 配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.repomixignore&lt;/code&gt;: 在提取代码时忽略的文件, 类似于 &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在 &lt;code&gt;.repomixignore&lt;/code&gt; 中加入以下不适合提供给 &lt;code&gt;AI&lt;/code&gt; 的文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public/*
src/assets/*
commitlint.config.ts
LICENSE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们尝试一下将文件上传至 &lt;a href=&quot;https://chat.deepseek.com&quot;&gt;deepseek&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/deepseek-repomix-chat-screenshot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://repomix.com/zh-cn/&quot;&gt;repomix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7329785321626664970&quot;&gt;二、人工智能之提示工程(Prompt Engineering)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 Lit 创建一个 AI 对话组件库 01 搭建篇</title><link>http://blog.xiaban.run/posts/2025/hyosan-chat-01-create/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/hyosan-chat-01-create/</guid><description>最近学习了一下 Lit / Web Components, 尝试使用 Lit 创建一个 AI 对话组件库</description><pubDate>Wed, 26 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本篇将介绍如何使用 &lt;code&gt;vite&lt;/code&gt; 创建并初始化一个 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; + &lt;a href=&quot;https://shoelace.style/&quot;&gt;shoelace&lt;/a&gt; 项目&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-01-create/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 01 搭建篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-02-prompts/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 02 Prompts 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-03-feasibility/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 03 可行性验证 篇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../hyosan-chat-04-i18n/&quot;&gt;使用 Lit 创建一个 AI 对话组件库 04 国际化 篇&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;此系列文章仅作为项目搭建过程的记录, 可能会忽略大量细节, 并且可能中道崩殂, 仅作为学习参考;&lt;/p&gt;
&lt;p&gt;如果你对 &lt;code&gt;Lit&lt;/code&gt; / &lt;code&gt;Web Components&lt;/code&gt; 有兴趣, 请查看我的另外两篇文章:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;../lit-initial-experience/&quot;&gt;Lit 初体验&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../web-components/&quot;&gt;web components 原生 js 实现自定义组件&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm create vite
✔ Project name: … hyosan-chat
✔ Select a framework: › Lit
✔ Select a variant: › TypeScript

Scaffolding project in /Users/xxx/projects/hyosan-chat...

Done. Now run:

  cd hyosan-chat
  pnpm install
  pnpm run dev

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
在创建项目时选择 &lt;strong&gt;Lit &amp;amp; TypeScript&lt;/strong&gt;, 下面我们先来搭建项目工程化所需的相关依赖 👇🏻
:::&lt;/p&gt;
&lt;h3&gt;git&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cd my-chat
pnpm i # 安装依赖
git init # 初始化 git 仓库
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;biome&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://biomejs.dev/zh-cn/guides/getting-started/&quot;&gt;biome&lt;/a&gt; 是一个使用 &lt;code&gt;Rust&lt;/code&gt; 编写的 &lt;strong&gt;代码检查&lt;/strong&gt; 和 &lt;strong&gt;格式化&lt;/strong&gt; 工具, 相比于 &lt;code&gt;eslint&lt;/code&gt; / &lt;code&gt;prettier&lt;/code&gt;, 它具有更好的 &lt;strong&gt;性能&lt;/strong&gt; 和 &lt;strong&gt;速度&lt;/strong&gt;, 并且 &lt;strong&gt;开箱即用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装 biome
pnpm add --save-dev --save-exact @biomejs/biome
# 生成配置文件 biome.json
pnpm biome init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改配置文件 &lt;code&gt;biome.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;$schema&quot;: &quot;https://biomejs.dev/schemas/1.9.4/schema.json&quot;,
	&quot;vcs&quot;: {
		&quot;enabled&quot;: true,
		&quot;clientKind&quot;: &quot;git&quot;,
		&quot;useIgnoreFile&quot;: true
	},
	&quot;files&quot;: {
		&quot;ignoreUnknown&quot;: false,
		&quot;ignore&quot;: []
	},
	&quot;formatter&quot;: {
		&quot;enabled&quot;: true,
		&quot;indentStyle&quot;: &quot;tab&quot;
	},
	&quot;organizeImports&quot;: {
		&quot;enabled&quot;: true
	},
	&quot;linter&quot;: {
		&quot;enabled&quot;: true,
		&quot;rules&quot;: {
			&quot;recommended&quot;: true,
			&quot;correctness&quot;: {
				&quot;noUnknownUnit&quot;: &quot;off&quot;
			}
		}
	},
	&quot;javascript&quot;: {
		&quot;formatter&quot;: {
			&quot;quoteStyle&quot;: &quot;single&quot;,
			&quot;semicolons&quot;: &quot;asNeeded&quot;
		}
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了方便使用, 我们将 &lt;code&gt;biome&lt;/code&gt; 命令加入到 &lt;code&gt;package.json scripts&lt;/code&gt; 中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;lint&quot;: &quot;biome check&quot;,
    &quot;lint:fix&quot;: &quot;biome check --write&quot;,   
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就可以直接通过 &lt;code&gt;pnpm run lint&lt;/code&gt; / &lt;code&gt;pnpm run lint:fix&lt;/code&gt; 执行格式化了&lt;/p&gt;
&lt;p&gt;然后安装 编辑器插件, 我用的是 &lt;code&gt;vscode&lt;/code&gt;, 直接安装 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=biomejs.biome&quot;&gt;Biome VSCode 扩展&lt;/a&gt;, 详见 &lt;a href=&quot;https://biomejs.dev/zh-cn/guides/integrate-in-editor/&quot;&gt;在编辑器中集成Biome&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后创建 &lt;code&gt;.vscode/extensions.json&lt;/code&gt; 文件, 添加如下内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;recommendations&quot;: [&quot;biomejs.biome&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
添加 &lt;code&gt;.vscode/extensions.json&lt;/code&gt; 是为了向其他开发者推荐安装此扩展(&lt;code&gt;biome&lt;/code&gt; 扩展)
:::&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;.vscode/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;editor.defaultFormatter&quot;: &quot;biomejs.biome&quot;, // 将 biome 设置为默认格式化器
  &quot;[javascript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;biomejs.biome&quot;
  },
  &quot;[typescript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;biomejs.biome&quot;
  },
  &quot;editor.formatOnSave&quot;: true,
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll.biome&quot;: &quot;always&quot;, // 保存时自动 fixed
    &quot;source.organizeImports.biome&quot;: &quot;always&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;cz-git&lt;/h3&gt;
&lt;p&gt;参考我的另一篇文章: &amp;lt;a href=&quot;../cz-git/&quot; target=&quot;_blank&quot;&amp;gt;🔗 使用 ChatGPT 生成 git message&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;git hooks 相关&lt;/h3&gt;
&lt;p&gt;:::warning
在上一步(&lt;a href=&quot;#cz-git&quot;&gt;cz-git&lt;/a&gt;)已经中安装并配置了 &lt;code&gt;commitlint&lt;/code&gt; / &lt;code&gt;commitizen&lt;/code&gt;, 所以这里不在提及
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm install -D simple-git-hooks lint-staged
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;package.json&lt;/code&gt; 文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
+  &quot;simple-git-hooks&quot;: {
+    &quot;pre-commit&quot;: &quot;npx lint-staged&quot;,
+    &quot;commit-msg&quot;: &quot;npx commitlint --edit ${1}&quot;
+  },
+  &quot;lint-staged&quot;: {
+    &quot;*.{html,css,json,md,vue,js,ts,jsx,tsx}&quot;: &quot;pnpm run lint:fix&quot;
+  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Update ./git/hooks
npx simple-git-hooks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 让我们测试一下是否生效:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -A # 添加所有文件
pnpm run cz ai # 使用 ChatGPT 生成 git message
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;shoelace&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://shoelace.style/&quot;&gt;shoelace&lt;/a&gt; 是 &lt;code&gt;web components&lt;/code&gt; 生态中最成熟的基础 &lt;code&gt;UI&lt;/code&gt; 组件库, 有了它, 我们可以很方便的使用它来构建我们的对话组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i @shoelace-style/shoelace
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;添加 demo 代码&lt;/h2&gt;
&lt;p&gt;简单的写一个 &lt;code&gt;hyosan-chat&lt;/code&gt; 组件, 并在 &lt;code&gt;index.html&lt;/code&gt; 中展示&lt;/p&gt;
&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/vite.svg&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Vite + Lit + TS&amp;lt;/title&amp;gt;

    &amp;lt;!-- ⚠️ 这里直接按照官方文档使用 link cdn 标签引入了样式, 用来实现 主题切换 --&amp;gt;
    &amp;lt;!-- ⚠️ 切换功能会在后续章节中实现, 实现方式暂未确定 --&amp;gt;

    &amp;lt;link
      rel=&quot;stylesheet&quot;
      media=&quot;(prefers-color-scheme:light)&quot;
      href=&quot;https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.0/cdn/themes/light.css&quot;
    /&amp;gt;
    &amp;lt;link
      rel=&quot;stylesheet&quot;
      media=&quot;(prefers-color-scheme:dark)&quot;
      href=&quot;https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.0/cdn/themes/dark.css&quot;
      onload=&quot;document.documentElement.classList.add(&apos;sl-theme-dark&apos;);&quot;
    /&amp;gt;

    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;./src/index.css&quot; /&amp;gt;
    &amp;lt;script type=&quot;module&quot; src=&quot;/src/main.ts&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;demo-container&quot;&amp;gt;
      &amp;lt;hyosan-chat&amp;gt;&amp;lt;/hyosan-chat&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
在代码中我们使用了最简单的 &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; 方式来引入 &lt;code&gt;shoelace&lt;/code&gt; 样式, 在后续章节中我们会将其改为其他方式引入
:::&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/main.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;@shoelace-style/shoelace/dist/themes/light.css&apos;
export * from &quot;./components&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;index.css&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html, body, h1, h2, h3, h4, p { padding: 0; margin: 0; }
.demo-container {
  width: 100wh;
  height: 100vh;
  display: flex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt; 是我们用来展示组件的 &lt;code&gt;demo&lt;/code&gt; 页面, 这里修改了一下样式, 使得组件的容器元素 &lt;code&gt;.demo-container&lt;/code&gt; 宽高 &lt;code&gt;100%&lt;/code&gt;, 并消除了浏览器默认的 &lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;margin&lt;/code&gt; 样式&lt;/p&gt;
&lt;p&gt;增加 &lt;code&gt;vite.config.ts&lt;/code&gt; 用于指定端口号(多个 &lt;code&gt;vite&lt;/code&gt; 项目共用端口号会造成浏览器缓存更新带来的各种问题):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import { resolve } from &apos;node:path&apos;

export default defineConfig({
  server: {
    port: 29510,
  },
  preview: {
    port: 29511,
  },
  resolve: {
    alias: {
      &apos;@&apos;: resolve(__dirname, &apos;src&apos;),
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/index.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export { HyosanChat } from &quot;./hyosan-chat&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/components/hyosan-chat.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { LitElement, css, html } from &quot;lit&quot;;
import { customElement, property } from &quot;lit/decorators.js&quot;;
import &apos;@shoelace-style/shoelace/dist/components/button/button.js&apos;

@customElement(&quot;hyosan-chat&quot;)
export class HyosanChat extends LitElement {
	static styles? = css`
		:host {
			width: 100%;
			height: 100%;
		}	
	`;

	@property({ reflect: true })
	message = &quot;Hello Lit&quot;;

	render() {
		return html`
      &amp;lt;h2&amp;gt;HyosanChat Component&amp;lt;/h2&amp;gt;
      &amp;lt;div&amp;gt;Hello Lit!&amp;lt;/div&amp;gt;
			&amp;lt;sl-button variant=&quot;primary&quot;&amp;gt;Hello Shoelace&amp;lt;/sl-button&amp;gt;
    `;
	}
}

declare global {
	interface HTMLElementTagNameMap {
		&quot;hyosan-chat&quot;: HyosanChat;
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;README.md&lt;/h2&gt;
&lt;p&gt;最后我们新增 &lt;code&gt;README.md&lt;/code&gt;, 用来描述我们的组件库:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# hyosan-chat
&amp;gt; 🚧 Work in Progress | 此项目处于早期开发阶段
&amp;gt;
&amp;gt; hyosan-chat is currently in active development and not usable for production yet.

## 介绍
这是一个使用 [Lit@3](https://lit.dev) &amp;amp; [shoelace](https://shoelace.style/) 创建一个 `AI` 对话组件库

## 技术栈
- [Lit@^3.2.1](https://lit.dev): `Web Component` 库
- [shoelace@^2.20.0](https://shoelace.style/): 使用 `Web Components` 实现的 `UI` 组件库
- [vite@^6.1.0](https://github.com/vitejs/vite): 现代化的前端构建工具
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;code&gt;README.md&lt;/code&gt; 文件非常重要, 他是开发者了解整个项目的重要入口
:::&lt;/p&gt;
&lt;h2&gt;编辑器自动补全插件&lt;/h2&gt;
&lt;p&gt;这里我是用的是 &lt;a href=&quot;https://lingma.aliyun.com/lingma/download&quot;&gt;通义灵码&lt;/a&gt;, &lt;a href=&quot;https://docs.github.com/zh/copilot/using-github-copilot/getting-code-suggestions-in-your-ide-with-github-copilot&quot;&gt;copilot&lt;/a&gt; 也是不错的选择&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biomejs.dev/zh-cn/guides/getting-started/&quot;&gt;biome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;../cz-git/&quot;&gt;使用 ChatGPT 生成 git message&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/toplenboren/simple-git-hooks/tree/master&quot;&gt;simple-git-hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shoelace.style/&quot;&gt;shoelace&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Lit 初体验</title><link>http://blog.xiaban.run/posts/2025/lit-initial-experience/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/lit-initial-experience/</guid><description>Lit 是一个由 Google 开发的基于 web components 的库, 依托 web components 计数, 可以在任意前端项目中使用(vue / react / angular / ...)</description><pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;背景篇(点击展开)&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;h2&gt;缘起&lt;/h2&gt;
&lt;p&gt;最近接到了一个新需求, 要开发一个与 &lt;code&gt;AI&lt;/code&gt; 进行对话的 &lt;code&gt;demo&lt;/code&gt;(组件), 类似于 &lt;a href=&quot;https://chat.openai.com&quot;&gt;chatgpt 网页版&lt;/a&gt;, 需要满足以下条件:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要 &lt;strong&gt;在现有框架(&lt;code&gt;vue3&lt;/code&gt; / &lt;code&gt;ant-design-vue@3&lt;/code&gt;)中使用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;既要可以单独部署(&lt;strong&gt;单页面&lt;/strong&gt;), 又要能作为 &lt;strong&gt;组件&lt;/strong&gt; 集成到现有项目&lt;/li&gt;
&lt;li&gt;因为要在现有项目中引入(嵌入), 所以尽量不引入过大的依赖, 保持 &lt;strong&gt;轻量&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;需求还未确定, 要有 &lt;strong&gt;良好的扩展性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;未来可能在使用其他技术栈(例如 &lt;code&gt;vue2&lt;/code&gt; / &lt;code&gt;react&lt;/code&gt; / &lt;code&gt;angular&lt;/code&gt;)的项目中使用, 需要有 &lt;strong&gt;良好的兼容性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实在开源社区已经有非常多的 &lt;a href=&quot;https://github.com/search?q=chat+ui&amp;amp;ref=opensearch&amp;amp;type=repositories&quot;&gt;chat ui&lt;/a&gt;, 但基本都是单独的项目, &lt;strong&gt;无法作为组件引入&lt;/strong&gt;, &lt;em&gt;似乎每个项目都立志的成为独具一格的产品&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;阿里的 &lt;a href=&quot;https://x.ant.design/index-cn&quot;&gt;ant-design-x&lt;/a&gt; 有 &lt;code&gt;vue&lt;/code&gt; 版本 &lt;a href=&quot;https://github.com/wzc520pyfm/ant-design-x-vue&quot;&gt;ant-design-vue-x&lt;/a&gt;, 它是一个成熟的组件库, 看起来满足我们的要求, 但它依赖于 &lt;code&gt;ant-design-vue@4&lt;/code&gt;, 我们的项目中使用的是 &lt;code&gt;ant-design-vue@3&lt;/code&gt;, 所以引入就报错了, 只能通过 &lt;code&gt;iframe&lt;/code&gt; 的方式引入 😭&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;wzc520pyfm/ant-design-x-vue&quot;}&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;经过一番苦寻, 终于找到了一个名为 &lt;a href=&quot;https://deepchat.dev/&quot;&gt;deepchat&lt;/a&gt; 的项目, 它基于 &lt;strong&gt;web components&lt;/strong&gt; 创建, 所以与前端基础框架无关, 更与组件库无关, 你甚至可以在纯 &lt;code&gt;html+css+js&lt;/code&gt; 项目中使用它&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;OvidijusParsiunas/deep-chat&quot;}&lt;/p&gt;
&lt;p&gt;将其引入到 &lt;code&gt;vue&lt;/code&gt; 项目中, 发现可以正常使用, 非常 &lt;code&gt;nice&lt;/code&gt; 😎, 关于 &lt;code&gt;vue&lt;/code&gt; 的 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; 兼容性, 可参考我的另一篇文章 &lt;a href=&quot;../web-components/#vue--web-components&quot;&gt;vue &amp;amp; web components&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/web-components.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;久闻 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; 大名, 之前工作中并未接触到它, 如今看到它无敌的兼容性, 留下了激动地泪水 😭; 在各种框架与库中兜兜转转, 最终回到了前端原生技术&lt;/p&gt;
&lt;p&gt;前端技术更新迭代这么多年, 新技术和新框架层出不穷, 生态割裂严重:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基础组件的开发者有时不得不为每个 &lt;strong&gt;前端框架&lt;/strong&gt; 都做一个 &lt;code&gt;adapter&lt;/code&gt;(&lt;code&gt;@xxx/vue&lt;/code&gt; / &lt;code&gt;@xxx/react&lt;/code&gt;  / &lt;code&gt;@xxx/angular&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;甚至为每个 &lt;strong&gt;框架的不同版本&lt;/strong&gt; 做兼容性处理(&lt;code&gt;vue2&lt;/code&gt; 和 &lt;code&gt;vue3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;像 &lt;a href=&quot;https://github.com/wzc520pyfm/ant-design-x-vue&quot;&gt;ant-design-vue-x&lt;/a&gt; 这样与组件库绑定的组件库, 甚至无法为 &lt;strong&gt;组件库的其他版本&lt;/strong&gt; 提供支持&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;什么是 Lit&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Lit is a simple library for building fast, lightweight web components.&lt;/p&gt;
&lt;p&gt;At Lit&apos;s core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that&apos;s tiny, fast and expressive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;引用官网的介绍:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 是一个用于构建 &lt;strong&gt;快速&lt;/strong&gt; / &lt;strong&gt;轻量级&lt;/strong&gt; &lt;code&gt;web components&lt;/code&gt; 的简单库&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 的核心是一个 &lt;strong&gt;能够消除样板代码的组件基类&lt;/strong&gt;, 它提供了 &lt;strong&gt;响应式状态&lt;/strong&gt; / &lt;strong&gt;作用域样式&lt;/strong&gt; 以及一个 &lt;strong&gt;声明式的模板系统&lt;/strong&gt;; 这个系统 小巧 / 快速 且 富有表现力&lt;/p&gt;
&lt;p&gt;:::tip
关于 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; 相关技术可以参考我的另一篇文章 &lt;a href=&quot;./web-components.md&quot;&gt;🔗 web components 原生 js 实现自定义组件&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;教程&lt;/h2&gt;
&lt;h3&gt;交互式教程&lt;/h3&gt;
&lt;p&gt;直接从 &amp;lt;a href=&quot;https://lit.dev/learn/#filter=tutorial&quot; target=&quot;_blank&quot;&amp;gt;🔗 交互式教程&amp;lt;/a&amp;gt; 开始看起, 这是一个 &lt;strong&gt;交互式&lt;/strong&gt; / &lt;strong&gt;带有 &lt;code&gt;playground&lt;/code&gt;&lt;/strong&gt; 的学习教程, 涵盖了 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 的所有特性&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;p&gt;如果要 &lt;strong&gt;快速入门&lt;/strong&gt; &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt;, &amp;lt;a href=&quot;https://lit.dev/playground/#sample=examples/hello-world&quot; target=&quot;_blank&quot;&amp;gt;🔗 examples&amp;lt;/a&amp;gt; 是一个更好的选择&lt;/p&gt;
&lt;p&gt;:::tip
从官方的教程搭配 &lt;code&gt;ChatGPT&lt;/code&gt; 入门是最好的选择, 这可以确保你快速找到问题的答案
:::&lt;/p&gt;
&lt;p&gt;首先使用 &lt;code&gt;vite&lt;/code&gt; 创建一个 &lt;code&gt;Lit&lt;/code&gt; 项目:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm create vite
✔ Project name: … lit-demo
✔ Select a framework: › Lit
✔ Select a variant: › TypeScript

Scaffolding project in /Users/xxx/projects/lit-demo...

Done. Now run:

  cd lit-demo
  pnpm install
  pnpm run dev

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { css, html, LitElement } from &quot;lit&quot;;
import { customElement, property, state } from &quot;lit/decorators.js&quot;;

@customElement(&apos;count-button&apos;)
export class CountButton extends LitElement {
  @property()
  message = &apos;Count is &apos;

  @state()
  count = 0
  handleClick() {
    this.count++
  }

  static styles = css`
    button {
      padding: 10px;
      font-size: 18px;
      border-radius: 10px;
      border-color: transparent;
    }
  `
  render() {
    return html`
      &amp;lt;button @click=${this.handleClick}&amp;gt;${this.message} ${this.count}&amp;lt;/button&amp;gt;
    `
  }
}

declare global {
  interface HTMLElementTagNameMap {
    &apos;count-button&apos;: CountButton
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
组件声明的标签名必须包含 &lt;code&gt;-&lt;/code&gt;(连字符), 这确保了与浏览器内置标签不会重复
:::&lt;/p&gt;
&lt;h2&gt;核心特性&lt;/h2&gt;
&lt;h3&gt;生命周期&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Lit&lt;/code&gt; 扩展了 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_custom_elements#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%85%83%E7%B4%A0%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%9B%9E%E8%B0%83&quot;&gt;&lt;code&gt;Web Components&lt;/code&gt; 的生命周期&lt;/a&gt;, 分为以下三个阶段, 详见 &lt;a href=&quot;https://lit.dev/docs/components/lifecycle/&quot;&gt;lifecycle&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;触发更新&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当 &lt;strong&gt;响应式属性值更新&lt;/strong&gt; / &lt;strong&gt;调用 &lt;code&gt;requestUpdate()&lt;/code&gt;&lt;/strong&gt; 时触发, &lt;code&gt;Lit&lt;/code&gt; 会触发异步更新, 即 &lt;strong&gt;捕获多个属性更改并体现到一个 &lt;code&gt;update&lt;/code&gt; 中&lt;/strong&gt;
&lt;img src=&quot;./assets/images/lit-lifecycle-change.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./assets/images/lit-lifecycle-change-schedule.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行更新&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时可以更新属性值, 更新后并不会触发重新 &lt;code&gt;update&lt;/code&gt;
&lt;img src=&quot;./assets/images/lit-lifecycle-update.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;完成更新
&lt;img src=&quot;./assets/images/lit-lifecycle-updated.jpg&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;生命周期函数&lt;/th&gt;
&lt;th&gt;继承自 &lt;code&gt;HTMLElement&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;执行方式&lt;/th&gt;
&lt;th&gt;常用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connectedCallback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;当元素被添加到文档中时调用&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disconnectedCallback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;当元素从文档中移除时调用&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adoptedCallback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;当元素被移动到新的文档时调用&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attributeChangedCallback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;当元素上的属性值发生变化时调用&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/properties/#haschanged&quot;&gt;hasChanged&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;在设置响应式属性时隐式调用(或 &lt;code&gt;@property&lt;/code&gt; 中声明)，用于 &lt;strong&gt;检查并决定是否触发更新&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#requestUpdate&quot;&gt;requestUpdate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;调用 &lt;code&gt;requestUpdate()&lt;/code&gt; 来安排显式更新。一般 &lt;strong&gt;用于与属性无关的内容发生更改时更新和呈现元素&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;调用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#shouldupdate&quot;&gt;shouldUpdate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;在更新开始前调用，用于 &lt;strong&gt;决定是否需要执行更新&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/tutorials/reactivity/#4&quot;&gt;✅&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#willupdate&quot;&gt;willUpdate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;在 &lt;code&gt;update()&lt;/code&gt; 之前调用以 &lt;strong&gt;计算 / 修改 更新期间所需的值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/tutorials/reactivity/#5&quot;&gt;✅&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#update&quot;&gt;update&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;调用以更新组件的 &lt;code&gt;DOM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#render&quot;&gt;render&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;由 &lt;code&gt;update()&lt;/code&gt; 调用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#firstupdated&quot;&gt;firstUpdated&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;在组件的 &lt;code&gt;DOM&lt;/code&gt; &lt;strong&gt;第一次更新后调用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#updated&quot;&gt;updated&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;每当组件的更新完成并且元素的 &lt;code&gt;DOM&lt;/code&gt; 已更新和呈现时调用&lt;/td&gt;
&lt;td&gt;声明&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/tutorials/reactivity/#6&quot;&gt;✅&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#updatecomplete&quot;&gt;updateComplete&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;值为 &lt;code&gt;Promise&amp;lt;boolean&amp;gt;&lt;/code&gt;, 表示组件是否完成更新, 可通过定义 &lt;a href=&quot;https://lit.dev/docs/components/lifecycle/#getUpdateComplete&quot;&gt;getUpdateComplete()&lt;/a&gt; 修改其行为&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;调用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;attribute &amp;amp; property&lt;/h3&gt;
&lt;p&gt;在 &lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 中有两个很容易混淆的概念: &lt;code&gt;attribute&lt;/code&gt; 和 &lt;code&gt;property&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;attribute&lt;/code&gt;: 指的是元素标签上的属性, 例如 &lt;code&gt;&amp;lt;my-element foo=&quot;bar&quot; /&amp;gt;&lt;/code&gt; 中的 &lt;code&gt;foo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;property&lt;/code&gt;: 指的是元素对象上的属性, 例如 &lt;code&gt;document.querySelector(&apos;my-element&apos;).foo&lt;/code&gt; 中的 &lt;code&gt;foo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
相比于 &lt;code&gt;attribute&lt;/code&gt;, &lt;code&gt;property&lt;/code&gt; 可以接受任意类型的值, 而 &lt;code&gt;attribute&lt;/code&gt; 只能接受字符串类型
:::&lt;/p&gt;
&lt;h2&gt;常用特性&lt;/h2&gt;
&lt;h3&gt;classMap&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { css, html, LitElement } from &apos;lit&apos;;
import { customElement, property, state } from &apos;lit/decorators.js&apos;;
import { classMap } from &apos;lit/directives/class-map.js&apos;;

@customElement(&apos;my-component&apos;)
export class MyComponent extends LitElements {
  // ...
  @state() private playDirection: -1 | 1 = 1;
  render() {
    return html`&amp;lt;div class=&quot;${classMap({ backwards: this.playDirection === -1 })}&quot;&amp;gt;&amp;lt;/div&amp;gt;`
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;repeat&lt;/h3&gt;
&lt;p&gt;参考教程 &lt;a href=&quot;https://lit.dev/tutorials/working-with-lists/#6&quot;&gt;working with lists&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/?search=web%20components&quot;&gt;caniuse Custom Elements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7104055306396631076&quot;&gt;Web Components-LitElement实践&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用 ChatGPT 生成 git message</title><link>http://blog.xiaban.run/posts/2025/cz-git/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/cz-git/</guid><description>cz-git 是一个工程性更强，轻量级，高度自定义，输出标准格式的 Commitizen 适配器和 CLI</description><pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://cz-git.qbb.sh/zh/guide/&quot;&gt;cz-git&lt;/a&gt; 是一个基于 &lt;a href=&quot;https://commitlint.js.org/guides/getting-started.html&quot;&gt;commitlint&lt;/a&gt; 的交互式 &lt;code&gt;git&lt;/code&gt; 提交命令行工具, &lt;strong&gt;支持利用 AI 🤖 &lt;a href=&quot;#ai-%E7%94%9F%E6%88%90%E6%8F%90%E4%BA%A4%E4%BF%A1%E6%81%AF&quot;&gt;辅助生成提交信息&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/cz-git-screenshot.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm i -g commitizen # 全局安装 commitizen
pnpm i -D cz-git @commitlint/{cli,config-conventional} # 在项目中安装 cz-git / commitlint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
也可全局安装 &lt;code&gt;cz-git&lt;/code&gt;, 参考 &lt;a href=&quot;https://cz-git.qbb.sh/zh/guide/#%E5%85%A8%E5%B1%80%E4%BD%BF%E7%94%A8&quot;&gt;全局安装&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;package.json&lt;/code&gt; 中增加:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;config&quot;: {
    &quot;commitizen&quot;: {
      &quot;path&quot;: &quot;node_modules/cz-git&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;a href=&quot;https://commitlint.js.org/guides/getting-started.html&quot;&gt;commitlint&lt;/a&gt; 的配置文件 &lt;code&gt;commitlint.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { UserConfig } from &apos;cz-git&apos;

const config: UserConfig = {
	extends: [&apos;@commitlint/config-conventional&apos;],
  prompt: {
      alias: { fd: &apos;docs: fix typos&apos; },
      messages: {
          type: &apos;选择你要提交的类型 :&apos;,
          scope: &apos;选择一个提交范围（可选）:&apos;,
          customScope: &apos;请输入自定义的提交范围 :&apos;,
          subject: &apos;填写简短精炼的变更描述 :\n&apos;,
          body: &apos;填写更加详细的变更描述（可选）。使用 &quot;|&quot; 换行 :\n&apos;,
          breaking: &apos;列举非兼容性重大的变更（可选）。使用 &quot;|&quot; 换行 :\n&apos;,
          footerPrefixesSelect: &apos;选择关联issue前缀（可选）:&apos;,
          customFooterPrefix: &apos;输入自定义issue前缀 :&apos;,
          footer: &apos;列举关联issue (可选) 例如: #31, #I3244 :\n&apos;,
          confirmCommit: &apos;是否提交或修改commit ?&apos;,
      },
      types: [
          { value: &apos;feat&apos;, name: &apos;feat:     新增功能 | A new feature&apos; },
          { value: &apos;fix&apos;, name: &apos;fix:      修复缺陷 | A bug fix&apos; },
          { value: &apos;docs&apos;, name: &apos;docs:     文档更新 | Documentation only changes&apos; },
          { value: &apos;style&apos;, name: &apos;style:    代码格式 | Changes that do not affect the meaning of the code&apos; },
          { value: &apos;refactor&apos;, name: &apos;refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature&apos; },
          { value: &apos;perf&apos;, name: &apos;perf:     性能提升 | A code change that improves performance&apos; },
          { value: &apos;test&apos;, name: &apos;test:     测试相关 | Adding missing tests or correcting existing tests&apos; },
          { value: &apos;build&apos;, name: &apos;build:    构建相关 | Changes that affect the build system or external dependencies&apos; },
          { value: &apos;ci&apos;, name: &apos;ci:       持续集成 | Changes to our CI configuration files and scripts&apos; },
          { value: &apos;revert&apos;, name: &apos;revert:   回退代码 | Revert to a commit&apos; },
          { value: &apos;chore&apos;, name: &apos;chore:    其他修改 | Other changes that do not modify src or test files&apos; },
      ],
      useEmoji: false,
      emojiAlign: &apos;center&apos;,
      useAI: false,
      aiNumber: 1,
      themeColorCode: &apos;&apos;,
      scopes: [],
      allowCustomScopes: true,
      allowEmptyScopes: true,
      customScopesAlign: &apos;bottom&apos;,
      customScopesAlias: &apos;custom&apos;,
      emptyScopesAlias: &apos;empty&apos;,
      upperCaseSubject: false,
      markBreakingChangeMode: false,
      allowBreakingChanges: [&apos;feat&apos;, &apos;fix&apos;],
      breaklineNumber: 100,
      breaklineChar: &apos;|&apos;,
      skipQuestions: [],
      issuePrefixes: [
      // 如果使用 gitee 作为开发管理
          { value: &apos;link&apos;, name: &apos;link:     链接 ISSUES 进行中&apos; },
          { value: &apos;closed&apos;, name: &apos;closed:   标记 ISSUES 已完成&apos; },
      ],
      customIssuePrefixAlign: &apos;top&apos;,
      emptyIssuePrefixAlias: &apos;skip&apos;,
      customIssuePrefixAlias: &apos;custom&apos;,
      allowCustomIssuePrefix: true,
      allowEmptyIssuePrefix: true,
      confirmColorize: true,
      scopeOverrides: undefined,
      defaultBody: &apos;&apos;,
      defaultIssues: &apos;&apos;,
      defaultScope: &apos;&apos;,
      defaultSubject: &apos;&apos;,
  },
}

export default config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更多配置规则可参考 &lt;a href=&quot;https://commitlint.js.org/reference/configuration.html#config-via-file&quot;&gt;配置文件 - commitlint&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;AI 生成提交信息&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/cz-git-openai.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::warning
涉密项目建议使用本地或内部大模型, 防止源码泄露
:::&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cz-git.qbb.sh/zh/guide/&quot;&gt;cz-git&lt;/a&gt; 支持 &lt;a href=&quot;https://cz-git.qbb.sh/zh/recipes/openai&quot;&gt;OpenAI&lt;/a&gt; 或与 &lt;code&gt;OpenAI&lt;/code&gt; 兼容的 &lt;code&gt;API&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i -D cross-env czg # 建议作为项目依赖使用, 同样是为了防止源码泄露
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;package.json&lt;/code&gt; 中增加 &lt;code&gt;cz&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;cz&quot;: &quot;cross-env NODE_OPTIONS=&apos;--experimental-transform-types --disable-warning ExperimentalWarning&apos; czg&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
由于我们的配置文件(&lt;code&gt;commitlint.config.ts&lt;/code&gt;), 所以需要加入 &lt;em&gt;实验性参数&lt;/em&gt;, 详见 &lt;a href=&quot;https://cz-git.qbb.sh/zh/config/#typescript-%E6%A8%A1%E6%9D%BF&quot;&gt;TypeScript 模板&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;修改配置文件 &lt;code&gt;commitlint.config.ts&lt;/code&gt; 以启用 AI 生成功能:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { UserConfig } from &apos;cz-git&apos;

const config: UserConfig = {
  // ...
  useAI: true,
  aiModel: &apos;gpt-4o-mini&apos;,
  aiNumber: 6, // 生成 6 条信息提供给我们选择
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置好 &lt;code&gt;API&lt;/code&gt; 地址 和 &lt;code&gt;API key&lt;/code&gt;, 设置好后会写入 &lt;code&gt;~/.czrc&lt;/code&gt; 文件中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm run cz ai --api-endpoint=https://&amp;lt;your-path&amp;gt;/v1
pnpm run cz ai --api-key=&amp;lt;your-api-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来尝试一下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -A
pnpm run cz ai

? 选择你要提交的类型 : feat:     新增功能 | A new feature
ℹ Generating your AI commit subject...
? Select suitable subject by AI generated: update TypeScript formatter to Prettier and enhance commitlint config

###--------------------------------------------------------###
feat: update TypeScript formatter to Prettier and enhance commitlint config
###--------------------------------------------------------###

? 是否提交或修改commit ? Modify and additional message with prompt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认输出的是英文, 我们可以在 &lt;code&gt;commitlint.config.ts&lt;/code&gt; 添加 &lt;code&gt;prompt&lt;/code&gt; 来约束生成的提交信息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { UserConfig } from &apos;cz-git&apos;

const config: UserConfig = {
  // ...
  useAI: true,
  aiModel: &apos;gpt-4o-mini&apos;,
  aiNumber: 6,
+  aiQuestionCB({ maxSubjectLength, diff }) {
+    return `用完整句子为以下 Git diff 代码写一个有见解并简洁的 Git 中文提交消息，不加任何前缀，并且内容不能超过 ${maxSubjectLength} 个字符: \`\`\`diff\n${diff}\n\`\`\``
+  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后, 我们再次提交:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm run cz ai

&amp;gt; fuwari@0.0.1 cz /Users/xxx/projects/blog.xiaban.run
&amp;gt; cross-env NODE_OPTIONS=&apos;--experimental-transform-types --disable-warning ExperimentalWarning&apos; czg &quot;ai&quot;

czg@1.11.0

? 选择你要提交的类型 : feat:     新增功能 | A new feature
ℹ Generating your AI commit subject...
? Select suitable subject by AI generated: Use arrow keys or type to search
❯ 更新 TypeScript 的默认格式化工具为 Prettier，并调整 commitlint 配置以增强提交提示信息，同时添加 cross-env 和 czg 依赖
  更新 VSCode TypeScript 格式化工具为 Prettier，调整 commitlint 配置以支持更多提交类型，并添加 cross-env 依赖，优化项目构建配置
  更新 TypeScript 格式化工具为 Prettier，调整 commitlint 配置以支持更多类型，并添加 cross-env 依赖以优化环境变量设置
  更新 VSCode 配置以使用 Prettier 作为 TypeScript 的默认格式化工具，同时优化 commitlint 配置，新增支持的提交类型和依赖项
  更新 Visual Studio Code 的 TypeScript 格式化工具为 Prettier，并优化 commitlint 配置，增加对自定义问题前缀的支持
  修改了 VSCode 的 TypeScript 默认格式化工具为 Prettier，并更新了 commitlint 配置以支持更多提交类型，同时添加了 cross-env 和 czg 依赖

###--------------------------------------------------------###
feat: 更新 TypeScript 的默认格式化工具为 Prettier，并调整 commitlint 配置以增强提交提示信息，同时添加 cross-env 和 czg 依赖
###--------------------------------------------------------###

? 是否提交或修改commit ? Yes
[main cacbaf8] feat: 更新 TypeScript 的默认格式化工具为 Prettier，并调整 commitlint 配置以增强提交提示信息，同时添加 cross-env 和 czg 依赖
 8 files changed, 248 insertions(+), 61 deletions(-)
 create mode 100644 src/content/posts/2025/assets/images/cz-git-openai.gif
 create mode 100644 src/content/posts/2025/assets/images/cz-git-screenshot.gif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完结撒花 ❤️&lt;/p&gt;
&lt;h2&gt;20250-03-09 更新&lt;/h2&gt;
&lt;p&gt;commitlint 对于 &lt;code&gt;ts&lt;/code&gt; 文件的支持不太行 😡, 所以将文件名改回了 &lt;code&gt;commitlint.config.mjs&lt;/code&gt;, 又加入了自定义的 &lt;code&gt;tag&lt;/code&gt; &lt;code&gt;WIP&lt;/code&gt;, 完整代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// import { RuleConfigSeverity } from &apos;commitlint&apos;
import { defineConfig } from &apos;cz-git&apos;

const config = defineConfig({
	extends: [&apos;@commitlint/config-conventional&apos;],
	rules: {
		&apos;type-enum&apos;: [
			2,
			&apos;always&apos;,
			[
				&apos;build&apos;,
				&apos;chore&apos;,
				&apos;ci&apos;,
				&apos;docs&apos;,
				&apos;feat&apos;,
				&apos;fix&apos;,
				&apos;perf&apos;,
				&apos;refactor&apos;,
				&apos;revert&apos;,
				&apos;style&apos;,
				&apos;test&apos;,
				&apos;WIP&apos;,
			],
		],
		&apos;type-case&apos;: [0], // 禁用类型大小写检查
	},
	prompt: {
		alias: { fd: &apos;docs: fix typos&apos; },
		messages: {
			type: &apos;选择你要提交的类型 :&apos;,
			scope: &apos;选择一个提交范围（可选）:&apos;,
			customScope: &apos;请输入自定义的提交范围 :&apos;,
			subject: &apos;填写简短精炼的变更描述 :\n&apos;,
			body: &apos;填写更加详细的变更描述（可选）。使用 &quot;|&quot; 换行 :\n&apos;,
			breaking: &apos;列举非兼容性重大的变更（可选）。使用 &quot;|&quot; 换行 :\n&apos;,
			footerPrefixesSelect: &apos;选择关联issue前缀（可选）:&apos;,
			customFooterPrefix: &apos;输入自定义issue前缀 :&apos;,
			footer: &apos;列举关联issue (可选) 例如: #31, #I3244 :\n&apos;,
			confirmCommit: &apos;是否提交或修改commit ?&apos;,
		},
		types: [
			{ value: &apos;feat&apos;, name: &apos;feat:     新增功能 | A new feature&apos; },
			{ value: &apos;fix&apos;, name: &apos;fix:      修复缺陷 | A bug fix&apos; },
			{ value: &apos;WIP&apos;, name: &apos;WIP:      暂未完成 | Not yet completed&apos; },
			{
				value: &apos;docs&apos;,
				name: &apos;docs:     文档更新 | Documentation only changes&apos;,
			},
			{
				value: &apos;style&apos;,
				name: &apos;style:    代码格式 | Changes that do not affect the meaning of the code&apos;,
			},
			{
				value: &apos;refactor&apos;,
				name: &apos;refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature&apos;,
			},
			{
				value: &apos;perf&apos;,
				name: &apos;perf:     性能提升 | A code change that improves performance&apos;,
			},
			{
				value: &apos;test&apos;,
				name: &apos;test:     测试相关 | Adding missing tests or correcting existing tests&apos;,
			},
			{
				value: &apos;build&apos;,
				name: &apos;build:    构建相关 | Changes that affect the build system or external dependencies&apos;,
			},
			{
				value: &apos;ci&apos;,
				name: &apos;ci:       持续集成 | Changes to our CI configuration files and scripts&apos;,
			},
			{ value: &apos;revert&apos;, name: &apos;revert:   回退代码 | Revert to a commit&apos; },
			{
				value: &apos;chore&apos;,
				name: &apos;chore:    其他修改 | Other changes that do not modify src or test files&apos;,
			},
		],
		useEmoji: false,
		emojiAlign: &apos;center&apos;,
		useAI: true,
		aiNumber: 6,
		aiModel: &apos;gpt-4o-mini&apos;,
		aiQuestionCB({ maxSubjectLength, diff }) {
			return `用完整句子为以下 Git diff 代码写一个有见解并简洁的 Git 中文提交消息，不加任何前缀，并且内容不能超过 ${maxSubjectLength} 个字符: \`\`\`diff\n${diff}\n\`\`\``
		},
		themeColorCode: &apos;&apos;,
		scopes: [],
		allowCustomScopes: true,
		allowEmptyScopes: true,
		customScopesAlign: &apos;bottom&apos;,
		customScopesAlias: &apos;custom&apos;,
		emptyScopesAlias: &apos;empty&apos;,
		upperCaseSubject: false,
		markBreakingChangeMode: false,
		allowBreakingChanges: [&apos;feat&apos;, &apos;fix&apos;],
		breaklineNumber: 100,
		breaklineChar: &apos;|&apos;,
		skipQuestions: [],
		issuePrefixes: [
			// 如果使用 gitee 作为开发管理
			{ value: &apos;link&apos;, name: &apos;link:     链接 ISSUES 进行中&apos; },
			{ value: &apos;closed&apos;, name: &apos;closed:   标记 ISSUES 已完成&apos; },
		],
		customIssuePrefixAlign: &apos;top&apos;,
		emptyIssuePrefixAlias: &apos;skip&apos;,
		customIssuePrefixAlias: &apos;custom&apos;,
		allowCustomIssuePrefix: true,
		allowEmptyIssuePrefix: true,
		confirmColorize: true,
		scopeOverrides: undefined,
		defaultBody: &apos;&apos;,
		defaultIssues: &apos;&apos;,
		defaultScope: &apos;&apos;,
		defaultSubject: &apos;&apos;,
	},
})

export default config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将 &lt;code&gt;package.json&lt;/code&gt; 中的 &lt;code&gt;scripts&lt;/code&gt; 修改为 &lt;code&gt;&quot;cz&quot;: &quot;czg&quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &quot;cz&quot;: &quot;cross-env NODE_OPTIONS=&apos;--experimental-transform-types --disable-warning ExperimentalWarning&apos; czg&quot;
+ &quot;cz&quot;: &quot;czg&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pnpm remove cross-env
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cz-git.qbb.sh/zh/guide/&quot;&gt;cz-git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/commitizen/cz-cli&quot;&gt;commitizen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://commitlint.js.org/guides/getting-started.html&quot;&gt;commitlint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>web components 原生 js 实现自定义组件</title><link>http://blog.xiaban.run/posts/2025/web-components/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/web-components/</guid><description>前端世界本质上是由一个个组件搭建拼接而成的, web components 使我们能够使用原生 js 实现一个自定义组件</description><pubDate>Fri, 21 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 web components&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt; 是对 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; 的封装与抽象, &lt;strong&gt;其最核心的价值是让 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; 更加适应现代化的前端开发工作流&lt;/strong&gt;; 所以让我们 &lt;strong&gt;从 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt;&lt;/strong&gt; 开始讲起&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt; 想要实现的是让我们 &lt;strong&gt;用原生 js 编写一个组件&lt;/strong&gt;, 其核心概念由以下技术组成:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Custom Element&lt;/code&gt;: 自定义元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Shadow DOM&lt;/code&gt;: 影子 &lt;code&gt;DOM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HTML template&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; 可重用元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slot&lt;/code&gt; 插槽&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面我们从零开始, 一步一步实现一个 &lt;code&gt;web components&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建一个 vite 项目, framework 选择 Vanilla, variant 选择 TypeScript
pnpm create vite

# 使用 vscode 打开这个项目
cd web-components-demo &amp;amp;&amp;amp; code .

# 安装依赖并启动项目, 然后在浏览器打开 http://localhost:5173/
pnpm i &amp;amp;&amp;amp; pnpm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
你可能会好奇为什么要使用 &lt;code&gt;vite&lt;/code&gt; 来创建项目, &lt;code&gt;web components&lt;/code&gt; 明明不依赖于任何技术或库, &lt;strong&gt;这里使用 &lt;code&gt;vite&lt;/code&gt; 是为了热刷新和 &lt;code&gt;ts&lt;/code&gt; 支持&lt;/strong&gt;, 如果你不想使用 &lt;code&gt;vite&lt;/code&gt;, 可以直接创建一个 &lt;code&gt;html&lt;/code&gt; 文件, 或直接在 &lt;a href=&quot;https://vite.new/vanilla-ts&quot;&gt;🔗 官方模板项目&lt;/a&gt; 中操作
:::&lt;/p&gt;
&lt;p&gt;:::tip
这里直接在命令行中使用 &lt;code&gt;code&lt;/code&gt; 打开了这个项目, 如果你没有这个命令, 需要在 &lt;code&gt;vscode&lt;/code&gt; 中按下 &lt;code&gt;ctrl + shift + p&lt;/code&gt;, 输入 &lt;code&gt;Shell Command: Install &apos;code&apos; command in PATH&lt;/code&gt; 来启用
:::&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-web-components-page1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是 &lt;code&gt;vite&lt;/code&gt; 项目的默认页面, 接下来我们将完全使用 &lt;code&gt;web components&lt;/code&gt; 来实现页面中的 &lt;code&gt;count&lt;/code&gt; 按钮:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code src/lib.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src/main.ts&lt;/code&gt; 中添加:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;./lib.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Elements&lt;/h3&gt;
&lt;p&gt;下面我们在 &lt;code&gt;src/lib.ts&lt;/code&gt; 文件中编写代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 0️⃣ 自定义元素 - 计数按钮 */
class CountButton extends HTMLElement {
  private _count = 0

  /** 1️⃣ 元素被添加到页面中时执行 */ 
  constructor() {
    super()

    /** 2️⃣ shadow DOM */
    const shadow = this.attachShadow({ mode: &apos;open&apos; })

    /** 计数按钮 */
    const button = document.createElement(&apos;button&apos;)
    button.textContent = `count is ${this._count}` // 绑定元素内容
    button.addEventListener(&apos;click&apos;, () =&amp;gt; button.textContent = `count is ${++this._count}`) // 绑定点击事件

    /** 3️⃣ 样式元素 */
    const style = document.createElement(&apos;style&apos;)
    style.textContent = `
      button {
        border-radius: 8px;
        border: 1px solid transparent;
        padding: 0.6em 1.2em;
        font-size: 1em;
        font-weight: 500;
        font-family: inherit;
        background-color: #1a1a1a;
        cursor: pointer;
        transition: border-color 0.25s;
      }
      button:hover {
        border-color: #646cff;
      }
      button:focus,
      button:focus-visible {
        outline: 4px auto -webkit-focus-ring-color;
      }
    `

    shadow.appendChild(style) // 插入样式
    shadow.appendChild(button) // 插入元素
  }
}

// 4️⃣ 注册自定义元素
customElements.define(&apos;count-button&apos;, CountButton)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们将 &lt;code&gt;&amp;lt;count-button&amp;gt;&lt;/code&gt; 加入 &lt;code&gt;HTML&lt;/code&gt; 中, 修改 &lt;code&gt;src/main.ts&lt;/code&gt;, 在原有按钮下方加入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;div class=&quot;card&quot;&amp;gt;
      &amp;lt;button id=&quot;counter&quot; type=&quot;button&quot;&amp;gt;&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
+    &amp;lt;div&amp;gt;
+      &amp;lt;count-button&amp;gt;&amp;lt;/count-button&amp;gt;
+    &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/vite-web-components-page2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击下方的计数按钮, 你会发现 &lt;code&gt;count&lt;/code&gt; 数增加了, 下面我们逐一说明代码中用到的技术&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0️⃣: 自定义元素, 官方提到了两种实现方式:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;class CountButton extends HTMLElement&lt;/code&gt;: ✅ 常用方式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class CountButton extends HTMLButtonElement&lt;/code&gt;: ⚠️ 通过查看 &lt;a href=&quot;https://caniuse.com/?search=web%20components&quot;&gt;caniuse Custom Elements&lt;/a&gt; 发现, &lt;strong&gt;&lt;code&gt;is&lt;/code&gt; 属性在 &lt;code&gt;Safari&lt;/code&gt; 上有兼容问题&lt;/strong&gt;, 为避免不必要的麻烦, 不推荐此实现方式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;1️⃣: 生命周期回调函数, 一共有 &lt;code&gt;4&lt;/code&gt; 个 &lt;code&gt;API&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;connectedCallback()&lt;/code&gt;: 元素已添加到页面中, &lt;strong&gt;对应 &lt;code&gt;vue&lt;/code&gt; 的 &lt;code&gt;mounted&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disconnectedCallback()&lt;/code&gt;: 元素已从页面中移除, &lt;strong&gt;对应 &lt;code&gt;vue&lt;/code&gt; 的 &lt;code&gt;unMounted&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adoptedCallback()&lt;/code&gt;: 从一个文档进入另一个文档, 例如移动至 &lt;code&gt;iframe&lt;/code&gt; 中, 较少使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attributeChangedCallback(name, oldValue, newValue)&lt;/code&gt;: 在属性 更改 / 添加 / 移除 / 替换 时调用, &lt;strong&gt;用于监听组件属性变化&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2️⃣: &lt;code&gt;const shadow = this.attachShadow({ mode: &apos;open&apos; })&lt;/code&gt;, 创建了一个 &lt;strong&gt;可以被外部访问的节点&lt;/strong&gt;, 详见 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Element/attachShadow&quot;&gt;attchShadow API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;3️⃣: 这里直接复制了 &lt;code&gt;src/styles.css&lt;/code&gt; 中的按钮的样式, 因为 &lt;strong&gt;&lt;code&gt;Shadow DOM&lt;/code&gt; 中的元素无法继承文档中的已有的样式&lt;/strong&gt;, 内部样式与外部样式严格隔离, &lt;em&gt;虽然可以 &lt;a href=&quot;https://mdn.github.io/web-components-examples/popup-info-box-external-stylesheet/&quot;&gt;像这样&lt;/a&gt; 使用 &lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;&lt;/code&gt; 实现&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;4️⃣: 需要注册才能在文档中使用, &lt;strong&gt;对应 &lt;code&gt;vue&lt;/code&gt; 的 &lt;code&gt;app.component(&apos;MyComponent&apos;, MyComponent)&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你是一个已经熟悉 &lt;code&gt;TypeScript&lt;/code&gt; 和前端生态的 &lt;s&gt;老油条&lt;/s&gt; 开发者, 此时你应该已经发现, 这样的写法存在诸多问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1️⃣ 组件缺少状态管理, 状态改变时无法自动更新 &lt;code&gt;DOM&lt;/code&gt;, 需要手动监听并更新&lt;/li&gt;
&lt;li&gt;在 2️⃣ 3️⃣ 中编写的 &lt;code&gt;HTML&lt;/code&gt; / &lt;code&gt;CSS&lt;/code&gt; 太过于原始, 没有任何代码提示或者说类型约束&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;响应式实现&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;connectedCallback&lt;/code&gt; 中我们创建了按钮并绑定了点击事件, 点击后 &lt;code&gt;_count&lt;/code&gt; 自增, 但它是一个只能在组件内部使用的值, 我们将其改为 &lt;strong&gt;可以在外部定义和修改的值&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CountButton extends HTMLElement {
  /** 需要监听的属性 */
  static observedAttributes = [&apos;count&apos;]
  /** 数量 */
  private _count = 233

  /** 按钮元素 */
  private _button = document.createElement(&apos;button&apos;)
  /**
   * 更新按钮元素的内容
   * @param count 数量
   */
  private _updateButtonContent(count = this._count) {
    this._button.textContent = `count is ${count}`
  }

  attributeChangedCallback(name: string, oldValue: string, newValue: string) {
    console.log(`${name} changed from ${oldValue} to ${newValue}`) // 属性变化时触发
    if (name === &apos;count&apos;) {
      this._count = Number(newValue)
      this._updateButtonContent()
    }
  }

  constructor() {
    super()

    /** shadow DOM */
    const shadow = this.attachShadow({ mode: &apos;open&apos; })

    /** 计数按钮 */
    this._updateButtonContent()
    this._button.addEventListener(&apos;click&apos;, () =&amp;gt; this._updateButtonContent(++this._count)) // 绑定点击事件

    /** 样式元素 */
    const style = document.createElement(&apos;style&apos;)
    style.textContent = `
      button {
        border-radius: 8px;
        border: 1px solid transparent;
        padding: 0.6em 1.2em;
        font-size: 1em;
        font-weight: 500;
        font-family: inherit;
        background-color: #1a1a1a;
        cursor: pointer;
        transition: border-color 0.25s;
      }
      button:hover {
        border-color: #646cff;
      }
      button:focus,
      button:focus-visible {
        outline: 4px auto -webkit-focus-ring-color;
      }
      @media (prefers-color-scheme: light) {
        :root {
          color: #213547;
          background-color: #ffffff;
        }
        button {
          background-color: #f9f9f9;
        }
      }
    `

    shadow.appendChild(style) // 插入样式
    shadow.appendChild(this._button) // 插入元素
  }

  /** 元素被添加到页面中时执行 */
  connectedCallback() {
    // 如果元素未添加 count 属性, 那么就添加并设置为默认值
    if (!this.hasAttribute(&apos;count&apos;)) this.setAttribute(&apos;count&apos;, this._count.toString())
  }
}

// 注册自定义元素
customElements.define(&apos;count-button&apos;, CountButton)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们增加了内部状态 &lt;code&gt;_count&lt;/code&gt;, 增加了监听函数 &lt;code&gt;attributeChangedCallback&lt;/code&gt;, 并对按钮的内容渲染逻辑和点击事件进行了分离, 实现了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击按钮时 &lt;code&gt;count + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当没有为元素添加 &lt;code&gt;count&lt;/code&gt; 属性时, 自动为其添加 &lt;code&gt;count&lt;/code&gt; 属性&lt;/li&gt;
&lt;li&gt;当元素的 &lt;code&gt;count&lt;/code&gt; 属性更新时, 同步更新内部的状态值 &lt;code&gt;_count&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;至此就实现了 &lt;code&gt;vue&lt;/code&gt; 中的双向数据绑定的效果&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Custom Elements 常见错误&lt;/h3&gt;
&lt;h4&gt;在 constructor 中设置属性值&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class MyDemo extends HTMLElement {
  /** 需要监听的属性 */
  static observedAttributes = [&apos;count&apos;]

  constructor(
    private _count = 1,
  ) {
    super()
    this.setAttribute(&apos;count&apos;, this._count.toString())

    /** shadow DOM */
    const shadow = this.attachShadow({ mode: &apos;open&apos; })
    const content = document.createElement(&apos;div&apos;)
    content.textContent = this._count.toString()
    shadow.appendChild(content)
  }
}

customElements.define(&apos;my-demo&apos;, MyDemo)

// demo
const myDemo = document.createElement(&apos;my-demo&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意最后一行, 如果我们通过 createElement 的方式创建自定义元素, 并且在 constructor 中设置了属性值, 浏览器会报错:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/web-component-error1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
这是因为违反了 &lt;code&gt;createElement&lt;/code&gt; 调用者的期望, 即调用 &lt;code&gt;createElement&lt;/code&gt; 时, 可以理解为调用者预期的元素是无属性的空元素, 详见 &lt;a href=&quot;https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance&quot;&gt;Requirements for custom element constructors and reactions - HTML Standard&lt;/a&gt;
:::&lt;/p&gt;
&lt;h4&gt;在 constructor 中获取外部传入的属性值&lt;/h4&gt;
&lt;p&gt;有时我们希望在自定义组件初始化时根据外部传入的属性值处理组件的状态, 但在 &lt;strong&gt;使用 &lt;code&gt;createElement&lt;/code&gt; 时获取到的属性值是空的&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CountButton extends HTMLElement {
  constructor() {
    console.log(&apos;[constructor] test: &apos;, this.getAttribute(&apos;test&apos;))
  }
  connectedCallback() {
    console.log(&apos;[connectedCallback] test: &apos;, this.getAttribute(&apos;test&apos;))
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;HTML&lt;/code&gt; 中直接写入元素:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;count-button test=&quot;test&quot; /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[constructor] test:  test
[connectedCallback] test:  test
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;createElement&lt;/code&gt; 创建元素:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const cb = document.createElement(&apos;count-button&apos;)
cb.setAttribute(&apos;test&apos;, &apos;test&apos;)
document.body.appendChild(cb)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[constructor] test:  null
[connectedCallback] test:  test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
由此可见, 如果希望获取或设置元素的属性值, 应该在 &lt;code&gt;connectedCallback&lt;/code&gt; 中
:::&lt;/p&gt;
&lt;h3&gt;Shadow DOM&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/web-components-shadow-dom.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也许你已经发现, 在浏览器的 &lt;code&gt;Elements&lt;/code&gt; 中出现了一个 &lt;code&gt;#shadow-root&lt;/code&gt; 的节点, 它与其他普通的 &lt;code&gt;DOM&lt;/code&gt; 节点有明显的不同: &lt;strong&gt;&lt;code&gt;#shadow-root&lt;/code&gt; 内部的节点不受外部样式和 js 的影响&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/web-components-shadow-dom-queryselectorall.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也无法通过 &lt;code&gt;querySelectorAll&lt;/code&gt; 在文档中查找到此 &lt;code&gt;#shadow-root&lt;/code&gt; 内部的元素, &lt;strong&gt;&lt;code&gt;shadow DOM&lt;/code&gt; 提供了一道与外部文档隔离的屏障&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/web-components-shadow-dom-getElement.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但如果 &lt;code&gt;shadow DOM&lt;/code&gt; 声明为 &lt;code&gt;{ mode: &apos;open&apos; }&lt;/code&gt;, 则可以通过 &lt;code&gt;shadowRoot&lt;/code&gt; 属性访问内部的节点; 如果我们的组件不想被外部文档访问, 则可以改为 &lt;code&gt;{ mode: &apos;closed&apos; }&lt;/code&gt;, 此时自定义元素的 &lt;code&gt;shadow DOM&lt;/code&gt; 对外部文档来说就是 &lt;strong&gt;完全不可见&lt;/strong&gt; 的:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/web-components-shadow-dom-closed-getElement.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;使用 CSSStyleSheet 构造样式&lt;/h4&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;/static-demos/web-components-shadow-dom-css.html&quot; title=&quot;web-components-shadow-dom-css&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyComponent extends HTMLElement {
  constructor() {
    super()
    /** shadow DOM */
    const shadow = this.attachShadow({ mode: &apos;open&apos; })
    const content = document.createElement(&apos;div&apos;)
    content.textContent = &apos;CSSStyleSheet Demo&apos;

    const sheet = new CSSStyleSheet()
    sheet.replaceSync(&apos;div { color: red; font-size: 20px; }&apos;)

    shadow.adoptedStyleSheets.push(sheet)
    shadow.appendChild(content)
  }
}
customElements.define(&apos;my-component&apos;, MyComponent)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们单独声明了一个 &lt;code&gt;CSSStyleSheet&lt;/code&gt;, 并在 &lt;code&gt;shadown&lt;/code&gt; 的 &lt;code&gt;adopetdStyleSheets&lt;/code&gt; 中加入了该样式, &lt;code&gt;CSSStyleSheet&lt;/code&gt; 的意义在于:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;管理样式更加灵活&lt;/strong&gt;, 允许向 &lt;code&gt;shadow DOM&lt;/code&gt; 或其子元素添加多个 &lt;code&gt;sheet&lt;/code&gt; 模块, 并且可以动态修改样式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;便于共享样式&lt;/strong&gt;, 如果样式要被多个元素共同使用, 可以将 &lt;code&gt;sheet&lt;/code&gt; 提取到外部实现共享&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;使用 template&lt;/h4&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;/static-demos/web-components-shadow-dom-template.html&quot; title=&quot;web-components-shadow-dom-css&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;h1&amp;gt;H1 Title&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;template id=&quot;my-template&quot;&amp;gt;
  &amp;lt;style&amp;gt;
    div {
      color: red;
      font-size: 20px;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;div&amp;gt;Template Demo&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;my-component&amp;gt;&amp;lt;/my-component&amp;gt;
&amp;lt;my-component&amp;gt;&amp;lt;/my-component&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class MyComponent extends HTMLElement {
  constructor() {
    super()
    /** shadow DOM */
    const shadow = this.attachShadow({ mode: &apos;open&apos; })

    const myTemplate = document.getElementById(&apos;my-template&apos;)

    shadow.appendChild(myTemplate.content.cloneNode(true))
  }
}
customElements.define(&apos;my-component&apos;, MyComponent)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; 提供了一种声明式的写法, 并且像 &lt;code&gt;Shadow DOM&lt;/code&gt; 一样具有样式和 &lt;code&gt;js&lt;/code&gt; 隔离, 让我们可以实现样式及元素的复用(&lt;code&gt;cloneNode&lt;/code&gt;), 相比于最初的 &lt;a href=&quot;http://localhost:4321/posts/2025/web-components/#custom-elements&quot;&gt;编程式&lt;/a&gt; 更加直观, 也更贴近现代化的前端开发体验&lt;/p&gt;
&lt;h3&gt;template &amp;amp; slot&lt;/h3&gt;
&lt;p&gt;如果你熟悉 &lt;code&gt;vue&lt;/code&gt;, 那应该会对 &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; 感到非常亲切, 在 &lt;code&gt;vue&lt;/code&gt; 中我们可以写 &lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt;, 在 &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; 中同样可以!&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;200&quot; src=&quot;/static-demos/web-components-shadow-dom-slot.html&quot; title=&quot;web-components-shadow-dom-css&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
  h2 {
    text-decoration-line: underline;
  }
&amp;lt;/style&amp;gt;
&amp;lt;template id=&quot;my-title&quot;&amp;gt;
  &amp;lt;style&amp;gt;
    header {
      color: red;
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;header&amp;gt;
    &amp;lt;slot name=&quot;title&quot;&amp;gt;NEED TITLE&amp;lt;/slot&amp;gt;
  &amp;lt;/header&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script src=&quot;./web-components-shadow-dom-slot.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;my-title&amp;gt;
&amp;lt;/my-title&amp;gt;
&amp;lt;my-title&amp;gt;
  &amp;lt;h2 slot=&quot;title&quot;&amp;gt;title&amp;lt;/h2&amp;gt;
&amp;lt;/my-title&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class MyTitle extends HTMLElement {
  constructor() {
    super()
    /** shadow DOM */
    const shadow = this.attachShadow({ mode: &apos;open&apos; })

    const myTemplate = document.getElementById(&apos;my-title&apos;)

    shadow.appendChild(myTemplate.content.cloneNode(true))
  }
}
customElements.define(&apos;my-title&apos;, MyTitle)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
这里的 &lt;code&gt;&amp;lt;h2 slot=&quot;title&quot;&amp;gt;&lt;/code&gt; 即带有组件内的红色样式, 也有组件外部的下划线样式, 可见 &lt;code&gt;slot&lt;/code&gt; 元素与 &lt;code&gt;Shadow DOM&lt;/code&gt; 内部元素的不同点: &lt;strong&gt;&lt;code&gt;slot&lt;/code&gt; 元素(插槽) 是可以继承外部样式的&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h2&gt;局限性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生态孱弱, 发展缓慢&lt;/strong&gt;, 相比 vue / react, &lt;code&gt;web components&lt;/code&gt; 显然在生态上更加落后&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺乏响应式系统&lt;/strong&gt;, Vue 和 React 的核心优势之一是其响应式数据绑定系统, 可以让 UI 随着数据的变化自动更新, 而 &lt;code&gt;web components&lt;/code&gt; 需要手动实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式封闭&lt;/strong&gt;, 无法像 vue / react 一样灵活的继承与覆盖样式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发效率低下&lt;/strong&gt;, &lt;code&gt;web components&lt;/code&gt; 更加贴近底层, 无法像 &lt;code&gt;vue&lt;/code&gt; / &lt;code&gt;react&lt;/code&gt; 一样提供抽象能力&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;web components 的兼容性&lt;/h3&gt;
&lt;p&gt;:::warning
根据 &lt;a href=&quot;https://caniuse.com/?search=web%20components&quot;&gt;caniuse web components&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;web components&lt;/code&gt; 在 &lt;code&gt;Safari&lt;/code&gt; 上不支持 &lt;code&gt;is&lt;/code&gt; 属性, 故不支持扩展内置元素
:::&lt;/p&gt;
&lt;h2&gt;生态&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;可在 &lt;a href=&quot;https://www.webcomponents.org/&quot;&gt;webcomponents.org&lt;/a&gt; 搜索更多 &lt;code&gt;web components&lt;/code&gt; 组件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;基础框架&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://omi.cdn-go.cn/home/latest/zh/&quot;&gt;Omi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UI 组件库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://shoelace.style/&quot;&gt;💻 shoelace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hellof2e/quark-design&quot;&gt;📱 quark-design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://open-wc.org/guides/community/component-libraries/&quot;&gt;更多组件库 ...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;vue &amp;amp; web components&lt;/h2&gt;
&lt;p&gt;根据 &lt;a href=&quot;https://cn.vuejs.org/guide/extras/web-components.html#web-components-and-typescript&quot;&gt;vue 官方文档&lt;/a&gt; 对于 &lt;code&gt;web components&lt;/code&gt; 的描述:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们认为 &lt;code&gt;Vue&lt;/code&gt; 和 &lt;code&gt;Web Components&lt;/code&gt; 是互补的技术。&lt;code&gt;Vue&lt;/code&gt; 为使用和创建自定义元素提供了出色的支持。无论你是将自定义元素集成到现有的 &lt;code&gt;Vue&lt;/code&gt; 应用中，还是使用 &lt;code&gt;Vue&lt;/code&gt; 来构建和分发自定义元素都很方便&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;vue&lt;/code&gt; 似乎有意依托成熟的功能和抽象能力, 成为支持 &lt;code&gt;web components&lt;/code&gt; 的框架, 将 &lt;code&gt;web components&lt;/code&gt; 融入自己&lt;/p&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Custom Elements 的初始化逻辑应该写到 constructor 还是 connectedCallback?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;constructor&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;初始化自定义元素类的内部状态&lt;/li&gt;
&lt;li&gt;初始化 &lt;code&gt;shadow DOM&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connectedCallback&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;设置或读取外部设置的元素属性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev&quot;&gt;Lit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components&quot;&gt;web components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/?search=web%20components&quot;&gt;caniuse Custom Elements&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>fish shell 配置</title><link>http://blog.xiaban.run/posts/2025/fish-shell/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/fish-shell/</guid><description>使用 fish shell 让终端操作纵享丝滑</description><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;fish-shell/fish-shell&quot;}&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt;(&lt;code&gt;the friendly interactive shell&lt;/code&gt;) 是一个开源的终端命令行工具, 它具有许多功能, 如 &lt;strong&gt;自动补全 / 语法高亮 / 命令历史记录&lt;/strong&gt; 等&lt;/p&gt;
&lt;h3&gt;为什么使用 fish shell&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;开箱即用的 &lt;strong&gt;语法高亮&lt;/strong&gt; ✨&lt;/li&gt;
&lt;li&gt;开箱即用的 &lt;strong&gt;输入时建议&lt;/strong&gt; / &lt;strong&gt;基于历史记录补全&lt;/strong&gt; 🔥&lt;/li&gt;
&lt;li&gt;丰富的 &lt;strong&gt;插件&lt;/strong&gt; 🔌 /  &lt;strong&gt;主题&lt;/strong&gt; 🎨&lt;/li&gt;
&lt;li&gt;简洁友好的 &lt;strong&gt;语法&lt;/strong&gt; 🌿&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;兼容性&lt;/h3&gt;
&lt;p&gt;:::warning
&lt;code&gt;fish shell&lt;/code&gt; 与 &lt;code&gt;macOS&lt;/code&gt; 自带的 &lt;code&gt;zsh&lt;/code&gt; 最大的不同就是, &lt;strong&gt;&lt;code&gt;fish shell&lt;/code&gt; 不完全兼容 &lt;code&gt;bash&lt;/code&gt; &lt;code&gt;POSIX shell&lt;/code&gt;, 这会导致在 &lt;code&gt;fish&lt;/code&gt; 中执行一些 &lt;code&gt;shell&lt;/code&gt; 脚本时会报错&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;MacOS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install fish
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-add-repository ppa:fish-shell/release-3
sudo apt update
sudo apt install fish
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;windows&lt;/h3&gt;
&lt;p&gt;在 windows 上, &lt;a href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt; 可以在 &lt;a href=&quot;https://docs.microsoft.com/zh-cn/windows/wsl/install&quot;&gt;Windows Subsystem for Linux&lt;/a&gt; 上安装, 其他终端环境的兼容性不详, 更多信息可以参考 &lt;a href=&quot;https://github.com/fish-shell/fish-shell?tab=readme-ov-file#windows&quot;&gt;windows - fish shell&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;设置为 默认 shell&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;以 &lt;code&gt;macOS&lt;/code&gt; 为例&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查看 fish 路径&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;wihch fish
/opt/homebrew/bin/fish
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;尝试使用 chsh 命令设置 fish 为默认 shell&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;chsh -s /opt/homebrew/bin/fish
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;退出终端并打开新终端测试&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;exit # 退出当前终端
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开新终端:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 如果未进入 fish shell, 则需要在 /etc/shells 文件末尾增加 fish shell 的路径, 然后再次进入新终端测试
echo &quot;/opt/homebrew/bin/fish&quot; | sudo tee -a /etc/shells &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
至此 &lt;code&gt;fish shell&lt;/code&gt; 已经安装并设置为默认 shell, 但在某些 &lt;code&gt;Linux&lt;/code&gt; 系统上, 可能会遇到各种报错, 若无法解决, 也可以在每次进入终端后执行 &lt;code&gt;fish&lt;/code&gt; 进入 &lt;code&gt;fish shell&lt;/code&gt;
:::&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt; 的配置文件在 &lt;code&gt;~/.config/fish/config.fish&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;配置文件&lt;/h3&gt;
&lt;p&gt;在 &lt;a href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt; 的配置文件中一般会设置一些 &lt;strong&gt;环境变量 / function / fish 配置&lt;/strong&gt; 等, 以下是一些示例:&lt;/p&gt;
&lt;h4&gt;环境变量&lt;/h4&gt;
&lt;p&gt;将 &lt;code&gt;~/.cargo/bin&lt;/code&gt; 加入系统环境变量 &lt;code&gt;PATH&lt;/code&gt; 中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# rust
set -Ux CARGO_HOME $HOME/.cargo
set -Ux PATH $CARGO_HOME/bin $PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;function&lt;/h4&gt;
&lt;p&gt;增加用于设置代理的 &lt;code&gt;proxy function&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function proxy
    set -Ux all_proxy http://127.0.0.1:7890
    set -Ux http_proxy http://127.0.0.1:7890
    set -Ux https_proxy http://127.0.0.1:7890
    echo all_proxy=$all_proxy
    echo http_proxy=$http_proxy
    echo https_proxy=$https_proxy
end

function noproxy
    set -e all_proxy
    set -e http_proxy
    set -e https_proxy
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置成功后可直接执行 &lt;code&gt;proxy&lt;/code&gt; / &lt;code&gt;noproxy&lt;/code&gt; 来启用和关闭代理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy # 开启代理
noproxy # 关闭代理
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;fish 配置&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt; 也提供了一些配置项, 我这里只设置了 &lt;code&gt;vi key bindings&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.config/fish/config.fish&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish_vi_key_bindings
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;source ~/.config/fish/config.fish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 &lt;strong&gt;在终端命令行中就可以使用 &lt;code&gt;vim&lt;/code&gt; 的常用操作了&lt;/strong&gt;, 对于 &lt;code&gt;vim&lt;/code&gt; 党非常友好, 例如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;模式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Insert Mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Command Mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Visual Mode&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;光标移动: &lt;code&gt;hjkl&lt;/code&gt; / &lt;code&gt;0&lt;/code&gt; / &lt;code&gt;$&lt;/code&gt; / &lt;code&gt;w&lt;/code&gt; / &lt;code&gt;b&lt;/code&gt; / &lt;code&gt;e&lt;/code&gt; / &lt;code&gt;...&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编辑: &lt;code&gt;dd&lt;/code&gt; / &lt;code&gt;D&lt;/code&gt; / &lt;code&gt;u&lt;/code&gt; / &lt;code&gt;C-r&lt;/code&gt; / &lt;code&gt;...&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他: &lt;code&gt;/&lt;/code&gt; 搜索历史记录&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;fish_config&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fish_config&lt;/code&gt; 是一个图形化配置工具, 可以通过它来配置 &lt;code&gt;fish shell&lt;/code&gt; 的 &lt;code&gt;colors&lt;/code&gt; / &lt;code&gt;prompt&lt;/code&gt; / &lt;code&gt;functions&lt;/code&gt; / ...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish_config # 执行后会打开一个网页
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/fish_config_page.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
在选择好 colors / prompt 后, 需要点击 &lt;code&gt;Set Theme&lt;/code&gt; / &lt;code&gt;Set Prompt&lt;/code&gt; 才会生效, 已有的终端也需要 &lt;code&gt;source ~/.config/fish/config.fish&lt;/code&gt; 才会生效
:::&lt;/p&gt;
&lt;h2&gt;oh-my-fish&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;oh-my-fish/oh-my-fish&quot;}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;oh-my-fish&lt;/code&gt; 是一个开源的 &lt;code&gt;fish shell&lt;/code&gt; 的插件管理器&lt;/strong&gt;, 类似于 &lt;code&gt;zsh&lt;/code&gt; 的 &lt;code&gt;oh-my-zsh&lt;/code&gt;, 可以通过它来安装一些常用的 插件 / 主题&lt;/p&gt;
&lt;h3&gt;安装主题&lt;/h3&gt;
&lt;p&gt;在开源社区有非常多有意思的主题, 查看所有主题: &lt;a href=&quot;https://github.com/oh-my-fish/oh-my-fish/blob/master/docs/Themes.md&quot;&gt;Themes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;例如安装 &lt;a href=&quot;https://github.com/oh-my-fish/oh-my-fish/blob/master/docs/Themes.md#bobthefish&quot;&gt;bobthefish theme&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;omf install bobthefish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/bobthefish-preview.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;安装字体&lt;/h3&gt;
&lt;p&gt;:::tip
由于 &lt;a href=&quot;https://github.com/oh-my-fish/oh-my-fish/blob/master/docs/Themes.md#bobthefish&quot;&gt;bobthefish theme&lt;/a&gt; 使用了一些特殊符号, &lt;strong&gt;我们电脑中的默认字体中并没有这些符号, 所以需要安装 patched-fonts 来支持这些特殊符号&lt;/strong&gt;, 详见 &lt;a href=&quot;https://powerline.readthedocs.io/en/master/installation.html#patched-fonts&quot;&gt;patched-fonts&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;这里推荐从 &lt;a href=&quot;https://github.com/ryanoasis/nerd-fonts?tab=readme-ov-file#patched-fonts&quot;&gt;ryanoasis/nerd-fonts&lt;/a&gt; 找到自己喜欢的字体, 并在 &lt;a href=&quot;https://github.com/ryanoasis/nerd-fonts/releases&quot;&gt;releases&lt;/a&gt; 中下载对应的字体文件并在系统上安装, 字体文件比较多, 并不需要都安装, 选择一个系列的全选右键打开安装即可&lt;/p&gt;
&lt;p&gt;安装完字体后, 还需要在自己的终端软件中设置字体, 例如在 &lt;code&gt;iTerm2&lt;/code&gt; 中:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击 &lt;code&gt;iTerm2 &amp;gt; Settings &amp;gt; Profiles &amp;gt; Text&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;进入 &lt;code&gt;Text &amp;gt; Font&lt;/code&gt;, 选择 &lt;code&gt;Use a different font for non-ASCII text&lt;/code&gt; 并选择 &lt;code&gt;Non-ASCII Font&lt;/code&gt; 字体, 或直接在 &lt;code&gt;Font&lt;/code&gt; 中选择安装的字体&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/iTerm2-settings-text.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
如果启用了 &lt;code&gt;Hotkey Window&lt;/code&gt;, 需要同步设置 &lt;code&gt;Default&lt;/code&gt; / &lt;code&gt;Hotkey Window&lt;/code&gt;
:::&lt;/p&gt;
&lt;h2&gt;获取帮助&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fish shell&lt;/code&gt; 提供了一个帮助文档, 执行 &lt;code&gt;help&lt;/code&gt; 命令即可打开一个帮助文档网页&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;help
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fishshell.com/&quot;&gt;fish shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oh-my-fish/oh-my-fish&quot;&gt;oh-my-fish&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://starship.rs/zh-CN/guide/&quot;&gt;starshop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>服务器请求阿里邮箱服务器发送邮件</title><link>http://blog.xiaban.run/posts/2025/server-mail/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/server-mail/</guid><description>有时在服务器上触发某些事件时, 需要向用户发送邮件, 使用 msmtp 实现发送邮件, 如果已经开通了阿里邮箱, 就无需自己搭建邮件服务器</description><pubDate>Thu, 13 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有时在服务器上触发某些事件时, 需要向用户(主要是我自己)发送邮件, 使用 &lt;code&gt;msmtp&lt;/code&gt; 实现发送邮件, 如果已经开通了阿里邮箱, 就 &lt;strong&gt;无需自己搭建邮件服务器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::tip
之前我已经配置好了 &lt;a href=&quot;https://help.aliyun.com/document_detail/437165.html&quot;&gt;阿里邮箱 - 免费版&lt;/a&gt;(参考 &lt;a href=&quot;/posts/2025/configure-domain-name-mailbox/&quot;&gt;📚 阿里云配置域名邮箱&lt;/a&gt;), 但是阿里邮箱的 &lt;strong&gt;&lt;a href=&quot;https://help.aliyun.com/document_detail/2852847.html&quot;&gt;API 开放平台&lt;/a&gt; 免费版并不支持&lt;/strong&gt;, 所以无法通过 &lt;code&gt;API&lt;/code&gt; 实现发送邮件的功能, 但好在 阿里邮箱 &lt;strong&gt;允许第三方客户端收发邮件&lt;/strong&gt;, 所以只要 &lt;strong&gt;配置好阿里邮箱, 并通过 &lt;a href=&quot;https://wiki.archlinux.org/title/Msmtp&quot;&gt;msmtp&lt;/a&gt; 发送邮件即可&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- ## 域名解析&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;s&gt;&lt;code&gt;@ MX mail.example.com | 1&lt;/code&gt;&lt;/s&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这条 &lt;code&gt;MX&lt;/code&gt; 记录是表示 &lt;strong&gt;指定接收邮件的服务器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::tip
由于我 &lt;strong&gt;没有接收邮件的需求&lt;/strong&gt;(已经有 阿里邮箱 了), 所以忽略 &lt;code&gt;@ MX mail.example.com | 1&lt;/code&gt; 解析(与阿里邮箱的解析冲突)
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@ A mail.example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这条记录用来解析 &lt;code&gt;mail&lt;/code&gt; 子域名到自己的服务器&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@ TXT v=spf1 mx:mail.example.com -all&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这条记录用来 &lt;strong&gt;指定发送邮件的服务器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::warning
如果你像我一样在 &lt;code&gt;1&lt;/code&gt; 中没有配置 &lt;code&gt;MX&lt;/code&gt; 记录, 则此处的 &lt;code&gt;mx:mail.example.com&lt;/code&gt; 应该替换为 &lt;code&gt;a:mail.example.com&lt;/code&gt;(即 &lt;code&gt;2&lt;/code&gt; 中配置的 &lt;code&gt;A&lt;/code&gt; 记录)
:::&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SPF(Sender Policy Framework)&lt;/code&gt;是一种 &lt;code&gt;DNS TXT&lt;/code&gt; 记录，它用于指定哪些服务器有权代表你的域名发送邮件, 以减少垃圾邮件和邮件欺诈的风险, 格式为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v=spf1 [mechanisms] [modifiers]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;v=spf1&lt;/code&gt;: SPF 版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mechanisms&lt;/code&gt;: 表示允许的邮件服务器来源, 比如 &lt;code&gt;ip4&lt;/code&gt; / &lt;code&gt;mx&lt;/code&gt; / &lt;code&gt;include&lt;/code&gt; 等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modifiers&lt;/code&gt;: &lt;code&gt;-all&lt;/code&gt; 拒绝不匹配邮件 / &lt;code&gt;~all&lt;/code&gt; 软失败 / &lt;code&gt;+all&lt;/code&gt; 允许所有&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
如果配置了阿里邮箱:&lt;/p&gt;
&lt;p&gt;注意: 阿里邮箱已经有一条 &lt;code&gt;SPF&lt;/code&gt; 记录了(&lt;code&gt;@ TXT v=spf1 include:spf.qiye.aliyun.com -all&lt;/code&gt;), 这里只需要将加入 &lt;code&gt;mail.example.com&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ TXT v=spf1 include:spf.qiye.aliyun.com a:mail.example.com -all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;我的最终配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@ A mail.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ TXT v=spf1 include:spf.qiye.aliyun.com a:mail.example.com -all&lt;/code&gt; --&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;msmtp&lt;/h2&gt;
&lt;p&gt;配置好解析规则后, 就需要在服务器上安装并配置 &lt;a href=&quot;https://wiki.archlinux.org/title/Msmtp&quot;&gt;msmtp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;msmtp&lt;/code&gt; 是一个轻量级的 &lt;code&gt;msmtp&lt;/code&gt; 客户端(命令行工具), 适用于 &lt;strong&gt;只需要发送邮件的场景&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;安装 msmtp&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install msmtp msmtp-mta -y
# 安装时会提示是否启用 AppArmor, 直接选择 No
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 msmtp&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/msmtprc
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 设置默认账户
account default
# 启用 STARTTLS
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
# 465 端口使用的是 SSL/TLS, 而不是 STARTTLS
tls_starttls off
# 你的 SMTP 服务器
host smtp.qiye.aliyun.com
# 端口号（465 是 SMTPS，587 是 STARTTLS）
port 465
# 你的邮件账户
auth on
user no-reply@example.com
password your-password
# 发送者邮箱
from no-reply@example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿里邮箱只开放 &lt;code&gt;465&lt;/code&gt; 端口, 所以需要将 &lt;code&gt;tls_starttls&lt;/code&gt; 设置为 &lt;code&gt;off&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;example.com&lt;/code&gt; 替换为你的域名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里创建了 &lt;code&gt;msmtp&lt;/code&gt; 的配置文件, 配置文件也可以放到 &lt;code&gt;~/.msmtprc&lt;/code&gt;, 配置好后最好设置一下权限为 &lt;code&gt;600&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;配置阿里邮箱&lt;/h2&gt;
&lt;p&gt;我们 &lt;strong&gt;不在服务器自己搭建邮件服务, 直接通过阿里邮箱实现发送邮件&lt;/strong&gt;, 所以此场景下, 服务器需要安装用户发送邮件的客户端(&lt;code&gt;msmtp&lt;/code&gt;), 阿里邮箱需要创建账号并开启 &lt;code&gt;SMTP&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;创建 no-reply 账号&lt;/h3&gt;
&lt;p&gt;创建一个用于发送邮件的账号&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://qiye.aliyun.com&quot;&gt;阿里邮箱网页端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;进入 组织与用户 - 员工账号管理 - 新建账号(&lt;code&gt;no-reply@example.com&lt;/code&gt;), &lt;strong&gt;必须勾选 开启 IMAP/SMPT 服务&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置安全策略&lt;/h3&gt;
&lt;p&gt;阿里邮箱默认不允许第三方客户端登录, 需要在 &lt;strong&gt;安全管理 - 账号安全策略&lt;/strong&gt; 中进行配置&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 安全管理 - 账号安全策略&lt;/li&gt;
&lt;li&gt;(可选) &lt;strong&gt;自由端双重认证 &amp;gt; 启用范围 中排除 &lt;code&gt;no-reply@example.com&lt;/code&gt;&lt;/strong&gt;, 此时可以通过邮件客户端登录 &lt;code&gt;no-reply@example&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三方客户端安全&lt;/strong&gt;, 开启 &lt;strong&gt;允许使用第三方客户端&lt;/strong&gt;, 启用范围根据需要修改, 必须包含 &lt;code&gt;no-reply@example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定期修改密码提醒 &amp;gt; 例外账号&lt;/strong&gt;, 添加 &lt;code&gt;no-reply@example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;发送邮件&lt;/h2&gt;
&lt;p&gt;由于是在命令行中调用 &lt;code&gt;msmtp&lt;/code&gt; 发送邮件, 所以需要注意一下邮件内容的格式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Subject: 邮件标题
From: 邮件发送者 &amp;lt;no-reply@example.com&amp;gt;
To: 邮件接收者 &amp;lt;no-reply@example.com&amp;gt;

邮件内容
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;echo -e &quot;Subject: 这是一封测试发送的邮件, 来自服务器\n\n这里是邮件内容, 仅用于测试&quot; | msmtp me@test.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/server-mail-receiver.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::warning
如果内容过于简单, 可能会直接进入邮件接受者的垃圾箱
:::&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/Msmtp&quot;&gt;msmtp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/007sx/p/18347813&quot;&gt;Ubuntu Linux 搭建邮件服务器（postfix + dovecot）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://qiye.aliyun.com&quot;&gt;阿里邮箱网页端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.aliyun.com/document_detail/36576.html?spm=5176.21213303.J_v8LsmxMG6alneH-O7TCPa.7.7e842f3dt7Ez4P&amp;amp;scm=20140722.S_help@@%E6%96%87%E6%A1%A3@@36576._.ID_help@@%E6%96%87%E6%A1%A3@@36576-RL_465%E7%AB%AF%E5%8F%A3-LOC_llm-OR_ser-PAR1_2150440e17395222609746582e4052-V_4-RE_new5-P0_1-P1_0#:~:text=25-,465,-%E6%B3%A8%EF%BC%9A%E4%BB%A5%E4%B8%8B%E8%80%81&quot;&gt;阿里邮箱 - 客户端配置&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MacOS 安装及配置 PostgreSQL 17</title><link>http://blog.xiaban.run/posts/2025/macos-postgresql/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/macos-postgresql/</guid><description>使用 brew 安装及运行 PostgreSQL 17, 并进行基本的初始化配置</description><pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文将介绍在 &lt;code&gt;MacOS&lt;/code&gt; 下如何使用 &lt;code&gt;brew&lt;/code&gt; 安装并运行 &lt;code&gt;PostgreSQL 17&lt;/code&gt;, 并进行基本的初始化配置&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;brew install postgresql@17 # 指定安装 17.x
# brew install postgresql # 不指定版本, 当前默认安装的版本是 14

# 安装过程较慢, 可以配置代理

brew services start postgresql@17 # 启动服务
# brew services start postgresql # 不指定版本, 当前默认会启动 14
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;brew services list # 查看 postgresql 服务状态

Name          Status  User  File
mysql         none
nginx         none    root
postgresql@17 started xxxx ~/Library/LaunchAgents/homebrew.mxcl.postgresql@17.plist
unbound       none
v2ray         none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;Status&lt;/code&gt; 为 &lt;code&gt;error&lt;/code&gt;, 可以在日志文件(&lt;code&gt;tail /opt/homebrew/var/log/postgresql@17.log&lt;/code&gt;) 中查看具体的报错&lt;/p&gt;
&lt;p&gt;:::warning
一个常见的错误是使用 &lt;code&gt;sudo brew&lt;/code&gt; 启动服务, 会导致启动报错:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;root&quot; execution of the PostgreSQL server is not permitted.
The server must be started under an unprivileged user ID to prevent
possible system security compromise.  See the documentation for
more information on how to properly start the server.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该直接使用 &lt;code&gt;brew&lt;/code&gt; 启动服务
:::&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;在安装后数据库的默认用户时当前登录的用户, 可通过 &lt;code&gt;whoami&lt;/code&gt; 命令查看当前登录的用户, 并且只有一个名为 &lt;code&gt;postgres&lt;/code&gt; 的数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql -d postgres # 进入 postgres 数据库

psql (17.2 (Homebrew))
输入 &quot;help&quot; 来获取帮助信息.

postgres=# exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要直接通过 &lt;code&gt;psql&lt;/code&gt; 进入数据库, 需要创建一个与当前用户同名的数据库(假设当前用户是 &lt;code&gt;xxxx&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql -d postgres # 进入 postgres 数据库

postgres=# CREATE DATABASE xxxx OWNER xxxx;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE xxxx TO xxxx;
GRANT
postgres=# exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pgcli&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;dbcli/pgcli&quot;}&lt;/p&gt;
&lt;p&gt;:::tip
当我们使用 &lt;code&gt;psql&lt;/code&gt; 进入数据库时, 会发现在命令行中没有任何代码提示, 其实已经有一个名为 &lt;code&gt;pgcli&lt;/code&gt; 的命令行工具可以解决这个问题, 这是一个使用 &lt;code&gt;python&lt;/code&gt; 编写的有 &lt;strong&gt;自动补全和语法高亮&lt;/strong&gt; 的命令行工具
:::&lt;/p&gt;
&lt;h3&gt;安装 pgcli&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install pgcli # 通过 brew 安装
# pip install -U pgcli # 或者通过 pip 安装
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 pgcli&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Usage: pgcli [OPTIONS] [DBNAME] [USERNAME]

Options:
  -h, --host TEXT          Host address of the postgres database.
  -p, --port INTEGER       Port number at which the postgres instance is
                           listening.
  -U, --username TEXT      Username to connect to the postgres database.
  -u, --user TEXT          Username to connect to the postgres database.
  -W, --password           Force password prompt.
  -w, --no-password        Never prompt for password.
  --single-connection      Do not use a separate connection for completions.
  -v, --version            Version of pgcli.
  -d, --dbname TEXT        database name to connect to.
  --pgclirc FILE           Location of pgclirc file.
  -D, --dsn TEXT           Use DSN configured into the [alias_dsn] section of
                           pgclirc file.
  --list-dsn               list of DSN configured into the [alias_dsn] section
                           of pgclirc file.
  --row-limit INTEGER      Set threshold for row limit prompt. Use 0 to
                           disable prompt.
  --application-name TEXT  Application name for the connection.
  --less-chatty            Skip intro on startup and goodbye on exit.
  --prompt TEXT            Prompt format (Default: &quot;\u@\h:\d&amp;gt; &quot;).
  --prompt-dsn TEXT        Prompt format for connections using DSN aliases
                           (Default: &quot;\u@\h:\d&amp;gt; &quot;).
  -l, --list               list available databases, then exit.
  --auto-vertical-output   Automatically switch to vertical output mode if the
                           result is wider than the terminal width.
  --warn TEXT              Warn before running a destructive query.
  --ssh-tunnel TEXT        Open an SSH tunnel to the given address and connect
                           to the database from it.
  --log-file TEXT          Write all queries &amp;amp; output into a file, in addition
                           to the normal output destination.
  --help                   Show this message and exit.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们进入 数据库:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# xxxx 是在配置章节创建的数据库, 如果没有创建, 可以直接使用 pgcli postgres 进入 postgres 数据库
pgcli xxxx

Server: PostgreSQL 17.2 (Homebrew)
Version: 4.1.0
Home: http://pgcli.com
xxxx@/tmp:xxxx&amp;gt;



 [F2] Smart Completion: ON  [F3] Multiline: OFF  [F4] Emacs-mode  [F5] Explain: OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;底部显示: &lt;code&gt;[F2] Smart Completion: ON  [F3] Multiline: OFF  [F4] Emacs-mode  [F5] Explain: OFF&lt;/code&gt; 可以看到当前 pgcli 的配置&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;[F2]&lt;/code&gt; 为智能补全, &lt;code&gt;[F3]&lt;/code&gt; 为多行输入, &lt;code&gt;[F4]&lt;/code&gt; 为 emacs 模式, &lt;code&gt;[F5]&lt;/code&gt; 为解释器模式, &lt;code&gt;[F6]&lt;/code&gt; 为语法高亮, &lt;code&gt;[F7]&lt;/code&gt; 为自动补全&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;pgcli&lt;/code&gt; 默认是 &lt;code&gt;Eamcs mode&lt;/code&gt;, 对于 &lt;code&gt;vim&lt;/code&gt; 党来说首先要配置的就是修改为 &lt;code&gt;vim mode&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim ~/.config/pgcli/config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到 &lt;code&gt;vi = False&lt;/code&gt;, 将其改为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外, 如果你更习惯多行模式, 可以配置默认启用 &lt;code&gt;Multiline&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;multi_line = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;multiline&lt;/code&gt; 模式下, 只有在输入 &lt;code&gt;;&lt;/code&gt; 后才会执行, 可以直接换行
:::&lt;/p&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;默认的配置文件在 &lt;code&gt;/opt/homebrew/var/postgresql@17&lt;/code&gt; 目录下, 可通过执行 `` 查看配置文件位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql -c &quot;SHOW config_file&quot; # 查看 postgresql 配置文件位置
psql -c &quot;SHOW hba_file&quot; # 查看 pg_hba 配置文件位置
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置为允许远程连接&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;postgresql.conf&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;listen_addresses = &apos;*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;pg_hba.conf&lt;/code&gt; 末尾加入:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; host    all             all             0.0.0.0/0               md5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 修改配置后重启 postgresql 服务
brew services restart postgresql@17
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;连接远程数据库&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;psql -h 192.168.1.100 -p 5432 -U myuser -d mydatabase
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基础操作&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更多操作可以参考 &lt;a href=&quot;https://www.postgresql.org/docs/17/index.html&quot;&gt;官方文档 v17&lt;/a&gt; / &lt;a href=&quot;https://wiki.sqlfans.cn/postgresql/pg-std-using.html&quot;&gt;pg 使用规范&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;用户与权限&lt;/h3&gt;
&lt;h4&gt;创建用户&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;PostgreSQL&lt;/code&gt; 中，可以通过 &lt;code&gt;CREATE ROLE&lt;/code&gt; 命令创建用户。&lt;code&gt;CREATE ROLE&lt;/code&gt; 是一个通用命令，用于创建角色（包括用户和组角色）。用户本质上是一种可以登录的特殊角色&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE ROLE myuser WITH LOGIN PASSWORD &apos;mypassword&apos;;

-- 创建超级管理员账户
CREATE ROLE myuser WITH LOGIN PASSWORD &apos;mypassword&apos; SUPERUSER;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;超级管理员权限&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-- 为已有用户分配超级管理员权限
ALTER USER myuser WITH SUPERUSER;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;分配普通权限&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;-- 允许用户 myuser 连接到数据库 mydatabase
GRANT CONNECT ON DATABASE mydatabase TO myuser;

-- 允许用户 myuser 对 public 模式中的所有表执行 SELECT、INSERT、UPDATE 和 DELETE 操作
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO myuser;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 查看所有数据库
SELECT * FROM pg_database;

\list
-- 创建数据库
-- 创建完成后，可以使用 `\c mydatabase` 命令切换到该数据库
CREATE DATABASE mydatabase;

-- 删除数据库
DROP DATABASE IF EXISTS mydatabase;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age INT,
    department VARCHAR(50)
);

-- 查看所有表
\c dbname # 切换上下文数据库
\d
-- SELECT * FROM pg_tables WHERE schemaname = &apos;public&apos;;

-- 查看表结构
\d employees

-- 修改表结构
ALTER TABLE employees ADD COLUMN email VARCHAR(100); -- 新增列
ALTER TABLE employees RENAME COLUMN age TO age_in_years; -- 修改列名
ALTER TABLE employees DROP COLUMN email; -- 删除列
ALTER TABLE employees ALTER COLUMN age TYPE INT; -- 修改列数据类型
ALTER TABLE employees ALTER COLUMN age SET DEFAULT 18; -- 修改列默认值
ALTER TABLE employees ALTER COLUMN age DROP DEFAULT; -- 删除默认值
ALTER TABLE employees ALTER COLUMN age SET NOT NULL; -- 不为空
ALTER TABLE employees ALTER COLUMN age DROP NOT NULL; -- 可为空

-- 重命名表
ALTER TABLE employees RENAME TO new_employees;

-- 删除表
DROP TABLE IF EXISTS employees;

-- 删除表数据
TRUNCATE TABLE employees;

-- 创建索引
CREATE INDEX idx_employees_name ON employees (name);

-- 插入数据
INSERT INTO employees (name, age, department) VALUES (&apos;John Doe&apos;, 30, &apos;Marketing&apos;);

-- 查询数据
SELECT * FROM employees;

-- 更新数据
UPDATE employees SET age = 31 WHERE name = &apos;John Doe&apos;;

-- 删除数据
DELETE FROM employees WHERE name = &apos;John Doe&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;事务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;BEGIN; -- 开始事务

-- 执行一些操作 ...
UPDATE employees SET age = 31 WHERE name = &apos;John Doe&apos;; -- 更新第一条数据
SAVEPOINT my_savepoint;


COMMIT; -- 提交事务
ROLLBACK; -- 回滚事务
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;视图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE VIEW marketing_employees AS
SELECT * FROM employees WHERE department = &apos;Marketing&apos;;

-- 查询视图
SELECT * FROM marketing_employees;

-- 删除视图
DROP VIEW IF EXISTS marketing_employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;存储过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION add_employee(name VARCHAR, age INT, department VARCHAR)
RETURNS VOID AS $$
BEGIN
    INSERT INTO employees (name, age, department) VALUES (name, age, department);
END;
$$ LANGUAGE plpgsql;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用存储过程:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT add_employee(&apos;David&apos;, 28, &apos;Sales&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pg_dump mydatabase &amp;gt; mydatabase_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;恢复数据库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;psql -d mydatabase mydatabase_backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/17/index.html&quot;&gt;官方文档 v17&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.sqlfans.cn/postgresql/pg-std-using.html&quot;&gt;pg 使用规范&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>阿里云配置域名邮箱</title><link>http://blog.xiaban.run/posts/2025/configure-domain-name-mailbox/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/configure-domain-name-mailbox/</guid><description>使用阿里邮箱的免费版配置自己的域名邮箱</description><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当拥有自己的域名之后, 就可以使用 &lt;strong&gt;阿里邮箱(免费版)&lt;/strong&gt; 配置自己的 &lt;strong&gt;域名邮箱&lt;/strong&gt; 了, 例如域名是 &lt;code&gt;example.com&lt;/code&gt;, 就可以创建例如 &lt;code&gt;xxx@example.com&lt;/code&gt; 的邮箱, 并直接通过 阿里邮箱 进行邮件收发, 不需要自己搭建邮件系统&lt;/p&gt;
&lt;p&gt;:::tip
通过服务器直接发送邮件可以参考 &lt;a href=&quot;/posts/2025/server-mail/&quot;&gt;通过服务器发送邮件&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;1. 购买阿里邮箱(免费版)&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/aliyun-mail-pay.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入阿里邮箱文档 &lt;a href=&quot;https://help.aliyun.com/document_detail/437165.html&quot;&gt;阿里邮箱 - 版本介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;找到免费版, 点击 &lt;strong&gt;开通免费邮箱&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在邮箱域名中选择已有域名, 输入自己的域名, 购买时长选择 &lt;code&gt;5&lt;/code&gt; 年&lt;/li&gt;
&lt;li&gt;点击立即购买(&lt;em&gt;我这里已经购买过了&lt;/em&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 配置邮箱账号&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/aliyun-mail-password.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;a href=&quot;https://help.aliyun.com/document_detail/36698.html&quot;&gt;阿里邮箱 - 开通指南&lt;/a&gt; 文档页面&lt;/li&gt;
&lt;li&gt;按照 &lt;em&gt;一、设置邮箱管理员密码&lt;/em&gt; 先设置好邮箱密码, 这里可以看到 &lt;strong&gt;管理员账号&lt;/strong&gt; 为 &lt;code&gt;postmaster@example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;按照 &lt;em&gt;二、分配员工账号&lt;/em&gt; 操作, 进入 &lt;a href=&quot;https://qiye.aliyun.com/?spm=a2c4g.11186623.0.0.67282e6a1kYYTI&quot;&gt;阿里邮箱网页端&lt;/a&gt; 并登录 &lt;strong&gt;管理员账号&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;参照文档创建新账号&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 配置域名解析&lt;/h2&gt;
&lt;p&gt;直接参照 &lt;a href=&quot;https://help.aliyun.com/document_detail/36698.html?spm=a2c4g.11186623.help-menu-35466.d_4_1.190266f8DIfE4a&amp;amp;scm=20140722.H_36698._.OR_help-T_cn~zh-V_1#:~:text=%E8%BF%87%E6%AD%A4%E6%AD%A5%E9%AA%A4%E3%80%82-,%E5%9B%9B%E3%80%81%E6%B7%BB%E5%8A%A0%E8%A7%A3%E6%9E%90,-%E6%B8%A9%E9%A6%A8%E6%8F%90%E7%A4%BA%EF%BC%9A%E6%8C%89&quot;&gt;四、添加解析&lt;/a&gt; 进行操作, &lt;strong&gt;如果是在阿里云购买的域名, 就可以直接 一键添加邮箱解析&lt;/strong&gt;, 配置好域名解析后可能需要等几分钟才会生效&lt;/p&gt;
&lt;p&gt;:::warning
需要特别注意的是, 如果已经配置了根域名的解析(即主机记录为 &lt;code&gt;@&lt;/code&gt;, 记录类型为 &lt;code&gt;A&lt;/code&gt; 的解析规则), 在使用 &lt;strong&gt;一键添加邮箱解析&lt;/strong&gt; 时可能会提示冲突(“A”记录和“CNAME”记录有冲突，请暂停或删除现有的“CNAME”记录后重试), 但其实解析规则已添加, 可以正常收发邮件, 详见 &lt;a href=&quot;https://help.aliyun.com/zh/dns/dns-record-conflict-rules?spm=a2c4g.11186623.0.0.ea9c60b4rqeNNs&quot;&gt;解析记录冲突的解决办法&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;4. 测试邮箱&lt;/h2&gt;
&lt;p&gt;当配置好域名解析后, 就可以使用自己创建的账号登录 &lt;a href=&quot;https://qiye.aliyun.com/?spm=a2c4g.11186623.0.0.67282e6a1kYYTI&quot;&gt;阿里邮箱网页端&lt;/a&gt; 进行收发邮件了&lt;/p&gt;
&lt;h3&gt;4.1 配置邮件自动转发&lt;/h3&gt;
&lt;p&gt;如果你没有在手机上安装 阿里邮箱 app, 可能不会及时收到邮件, 可以在邮箱中配置邮件转发, 将收到的邮件邮件转发到自己的常用邮箱上:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/aliyun-mail-repost.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 阿里邮箱 网页端&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;设置 - 查看更多设置 - 邮件设置 - 自动转发&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;添加邮件转发地址&lt;/strong&gt; 添加自己的常用邮箱&lt;/li&gt;
&lt;li&gt;添加好后会收到一封 &lt;em&gt;请验证自动转发的邮件地址&lt;/em&gt; 的邮件, 点击链接进行验证&lt;/li&gt;
&lt;li&gt;验证成功后, 就可以 &lt;strong&gt;测试一下转发功能&lt;/strong&gt; 了(&lt;code&gt;test@test.com&lt;/code&gt; 向 &lt;code&gt;admin@example.com&lt;/code&gt; 发送邮件, 将会转发到我的常用邮箱中)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/aliyun-mail-repost-test.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;然后再 &lt;strong&gt;测试一下发送功能&lt;/strong&gt;(&lt;code&gt;admin@example.com&lt;/code&gt; 向 &lt;code&gt;test@test.com&lt;/code&gt; 发送邮件)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./assets/images/aliyun-mail-send.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.aliyun.com/document_detail/437165.html&quot;&gt;阿里邮箱 - 版本介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.aliyun.com/document_detail/36698.html&quot;&gt;阿里邮箱 - 开通指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://qiye.aliyun.com&quot;&gt;阿里邮箱网页端&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>systemctl 命令</title><link>http://blog.xiaban.run/posts/2025/systemctl_command/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/2025/systemctl_command/</guid><description>Linux 下的 systemctl 命令</description><pubDate>Mon, 27 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. &lt;code&gt;systemctl&lt;/code&gt; 简介&lt;/h2&gt;
&lt;p&gt;在 Linux 系统中, &lt;code&gt;systemctl&lt;/code&gt; 是一个强大的命令行工具, 用于管理和控制系统服务。它是 &lt;code&gt;systemd&lt;/code&gt; 系统和服务管理器的一部分, 广泛应用于现代 Linux 发行版, 包括 Ubuntu。本文将详细介绍 &lt;code&gt;systemctl&lt;/code&gt; 命令的使用, 并结合 &lt;code&gt;apt&lt;/code&gt; 包管理器、Ubuntu 系统以及常见的服务器软件（如 &lt;code&gt;nginx&lt;/code&gt;、&lt;code&gt;MySQL&lt;/code&gt; 等）进行讲解。&lt;/p&gt;
&lt;h3&gt;1.1 基本语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl [选项] &amp;lt;命令&amp;gt; &amp;lt;服务名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用的命令包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;start&lt;/code&gt;：启动服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stop&lt;/code&gt;：停止服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restart&lt;/code&gt;：重启服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reload&lt;/code&gt;：重新加载服务的配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enable&lt;/code&gt;：启用服务（开机自启）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disable&lt;/code&gt;：禁用服务（开机不自启）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;：查看服务状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list-units&lt;/code&gt;：列出所有已加载的服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 使用 &lt;code&gt;apt&lt;/code&gt; 安装和管理服务&lt;/h2&gt;
&lt;p&gt;在 Ubuntu 系统中, &lt;code&gt;apt&lt;/code&gt; 是默认的包管理工具, 用于安装、更新和删除软件包。我们可以使用 &lt;code&gt;apt&lt;/code&gt; 来安装常见的服务器软件, 如 &lt;code&gt;nginx&lt;/code&gt;、&lt;code&gt;MySQL&lt;/code&gt; 等, 然后使用 &lt;code&gt;systemctl&lt;/code&gt; 来管理这些服务。&lt;/p&gt;
&lt;h3&gt;2.1 安装 &lt;code&gt;nginx&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后, &lt;code&gt;nginx&lt;/code&gt; 服务会自动启动。我们可以使用 &lt;code&gt;systemctl&lt;/code&gt; 来查看其状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;● nginx.service - A high performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2023-10-10 12:34:56 UTC; 1min ago
     Docs: man:nginx(8)
 Main PID: 1234 (nginx)
    Tasks: 2 (limit: 1137)
   Memory: 4.0M
   CGroup: /system.slice/nginx.service
           ├─1234 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
           └─1235 nginx: worker process
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 安装 &lt;code&gt;MySQL&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install mysql-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后, &lt;code&gt;MySQL&lt;/code&gt; 服务会自动启动。我们可以使用 &lt;code&gt;systemctl&lt;/code&gt; 来管理它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 管理服务&lt;/h2&gt;
&lt;h3&gt;3.1 启动和停止服务&lt;/h3&gt;
&lt;p&gt;要启动 &lt;code&gt;nginx&lt;/code&gt; 服务, 可以使用以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要停止服务, 可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl stop nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 重启和重新加载服务&lt;/h3&gt;
&lt;p&gt;重启 &lt;code&gt;nginx&lt;/code&gt; 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是修改了配置文件, 可以使用 &lt;code&gt;reload&lt;/code&gt; 命令重新加载配置而不中断服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 启用和禁用服务&lt;/h3&gt;
&lt;p&gt;启用 &lt;code&gt;nginx&lt;/code&gt; 服务, 使其在系统启动时自动启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;禁用服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl disable nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 查看服务状态&lt;/h3&gt;
&lt;p&gt;查看 &lt;code&gt;nginx&lt;/code&gt; 服务的状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 其他常用命令&lt;/h2&gt;
&lt;h3&gt;4.1 列出所有服务&lt;/h3&gt;
&lt;p&gt;列出所有已加载的服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl list-units --type=service
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 查看服务的依赖关系&lt;/h3&gt;
&lt;p&gt;查看 &lt;code&gt;nginx&lt;/code&gt; 服务的依赖关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl list-dependencies nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 查看服务的日志&lt;/h3&gt;
&lt;p&gt;查看 &lt;code&gt;nginx&lt;/code&gt; 服务的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -u nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;systemctl&lt;/code&gt; 是 Linux 系统中管理服务的强大工具, 特别是在 Ubuntu 服务器环境中。通过结合 &lt;code&gt;apt&lt;/code&gt; 包管理器, 我们可以轻松安装和管理常见的服务器软件, 如 &lt;code&gt;nginx&lt;/code&gt;、&lt;code&gt;MySQL&lt;/code&gt; 等。掌握 &lt;code&gt;systemctl&lt;/code&gt; 的基本命令, 能够有效地管理服务器的服务, 确保系统的稳定运行。&lt;/p&gt;
&lt;p&gt;无论是启动、停止、重启服务, 还是查看服务状态、启用开机自启, &lt;code&gt;systemctl&lt;/code&gt; 都提供了简单而强大的功能。希望本文能够帮助你更好地理解和使用 &lt;code&gt;systemctl&lt;/code&gt;, 提升你在 Linux 服务器管理中的效率。&lt;/p&gt;
</content:encoded></item><item><title>Markdown Extended Features</title><link>http://blog.xiaban.run/posts/markdown-extended/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/markdown-extended/</guid><description>Read more about Markdown features in Fuwari</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GitHub Repository Cards&lt;/h2&gt;
&lt;p&gt;You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Fabrizz/MMM-OnSpotify&quot;}&lt;/p&gt;
&lt;p&gt;Create a GitHub repository card with the code &lt;code&gt;::github{repo=&quot;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&quot;}&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;saicaca/fuwari&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Admonitions&lt;/h2&gt;
&lt;p&gt;Following types of admonitions are supported: &lt;code&gt;note&lt;/code&gt; &lt;code&gt;tip&lt;/code&gt; &lt;code&gt;important&lt;/code&gt; &lt;code&gt;warning&lt;/code&gt; &lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::note
Highlights information that users should take into account, even when skimming.
:::&lt;/p&gt;
&lt;p&gt;:::tip
Optional information to help a user be more successful.
:::&lt;/p&gt;
&lt;p&gt;:::important
Crucial information necessary for users to succeed.
:::&lt;/p&gt;
&lt;p&gt;:::warning
Critical content demanding immediate user attention due to potential risks.
:::&lt;/p&gt;
&lt;p&gt;:::caution
Negative potential consequences of an action.
:::&lt;/p&gt;
&lt;h3&gt;Basic Syntax&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::note
Highlights information that users should take into account, even when skimming.
:::

:::tip
Optional information to help a user be more successful.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Titles&lt;/h3&gt;
&lt;p&gt;The title of the admonition can be customized.&lt;/p&gt;
&lt;p&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub Syntax&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot;&gt;The GitHub syntax&lt;/a&gt; is also supported.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; The GitHub syntax is also supported.

&amp;gt; [!TIP]
&amp;gt; The GitHub syntax is also supported.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spoiler&lt;/h3&gt;
&lt;p&gt;You can add spoilers to your text. The text also supports &lt;strong&gt;Markdown&lt;/strong&gt; syntax.&lt;/p&gt;
&lt;p&gt;The content :spoiler[is hidden &lt;strong&gt;ayyy&lt;/strong&gt;]!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The content :spoiler[is hidden **ayyy**]!

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Expressive Code Example</title><link>http://blog.xiaban.run/posts/expressive-code/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/expressive-code/</guid><description>How code blocks look in Markdown using Expressive Code.</description><pubDate>Wed, 10 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here, we&apos;ll explore how code blocks look using &lt;a href=&quot;https://expressive-code.com/&quot;&gt;Expressive Code&lt;/a&gt;. The provided examples are based on the official documentation, which you can refer to for further details.&lt;/p&gt;
&lt;h2&gt;Expressive Code&lt;/h2&gt;
&lt;h3&gt;Syntax Highlighting&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/syntax-highlighting/&quot;&gt;Syntax Highlighting&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Regular syntax highlighting&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;This code is syntax highlighted!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Rendering ANSI escape sequences&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ANSI colors:
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
- Bold:    [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
- Dimmed:  [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m

256 colors (showing colors 160-177):
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m

Full RGB colors:
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m

Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Editor &amp;amp; Terminal Frames&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/frames/&quot;&gt;Editor &amp;amp; Terminal Frames&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Code editor frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Title attribute example&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/content/index.html --&amp;gt;
&amp;lt;div&amp;gt;File name comment example&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Terminal frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;This terminal frame has no title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;This one has a title!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Overriding frame types&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Look ma, no frame!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;# Without overriding, this would be a terminal frame
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Text &amp;amp; Line Markers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/text-markers/&quot;&gt;Text &amp;amp; Line Markers&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Marking full lines &amp;amp; line ranges&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Line 1 - targeted by line number
// Line 2
// Line 3
// Line 4 - targeted by line number
// Line 5
// Line 6
// Line 7 - targeted by range &quot;7-8&quot;
// Line 8 - targeted by range &quot;7-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting line marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;this line is marked as deleted&apos;)
  // This line and the next one are marked as inserted
  console.log(&apos;this is the second inserted line&apos;)

  return &apos;this line uses the neutral default marker type&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding labels to line markers&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}
  value={value}
  className={buttonClassName}
  disabled={disabled}
  active={active}
&amp;gt;
  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding long labels on their own lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}

  value={value}
  className={buttonClassName}

  disabled={disabled}
  active={active}
&amp;gt;

  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Using diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+this line will be marked as inserted
-this line will be marked as deleted
this is a regular line
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
 no whitespace will be removed either
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Combining syntax highlighting with diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  function thisIsJavaScript() {
    // This entire block gets highlighted as JavaScript,
    // and we can still add diff markers to it!
-   console.log(&apos;Old code to be removed&apos;)
+   console.log(&apos;New and shiny code!&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Marking individual text inside lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  // Mark any given text inside lines
  return &apos;Multiple matches of the given text are supported&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Regular expressions&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;The words yes and yep will be marked.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Escaping forward slashes&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Test&quot; &amp;gt; /home/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting inline marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;These are inserted and deleted marker types&apos;);
  // The return statement uses the default marker type
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Word Wrap&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/word-wrap/&quot;&gt;Word Wrap&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Configuring word wrap per block&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Configuring indentation of wrapped lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent (enabled by default)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collapsible Sections&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/collapsible-sections/&quot;&gt;Collapsible Sections&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// All this boilerplate setup code will be collapsed
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// This part of the code will be visible by default
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
  // You can have multiple collapsed sections
  const a = 1
  const b = 2
  const c = a + b

  // This will remain visible
  console.log(`Calculation result: ${a} + ${b} = ${c}`)
  return c
}

// All this code until the end of the block will be collapsed again
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: &apos;End of example boilerplate code&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Line Numbers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/line-numbers/&quot;&gt;Line Numbers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Displaying line numbers per block&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// This code block will show line numbers
console.log(&apos;Greetings from line 2!&apos;)
console.log(&apos;I am on line 3&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Line numbers are disabled for this block
console.log(&apos;Hello?&apos;)
console.log(&apos;Sorry, do you know what line I am on?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Changing the starting line number&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Greetings from line 5!&apos;)
console.log(&apos;I am on line 6&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Simple Guides for Fuwari</title><link>http://blog.xiaban.run/posts/guide/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/guide/</guid><description>How to use this blog template.</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This blog template is built with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. For the things that are not mentioned in this guide, you may find the answers in the &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Front-matter of Posts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The title of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The date the post was published.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A short description of the post. Displayed on index page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cover image path of the post.&amp;lt;br/&amp;gt;1. Start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;: Use web image&amp;lt;br/&amp;gt;2. Start with &lt;code&gt;/&lt;/code&gt;: For image in &lt;code&gt;public&lt;/code&gt; dir&amp;lt;br/&amp;gt;3. With none of the prefixes: Relative to the markdown file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tags of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The category of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If this post is still a draft, which won&apos;t be displayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Where to Place the Post Files&lt;/h2&gt;
&lt;p&gt;Your post files should be placed in &lt;code&gt;src/content/posts/&lt;/code&gt; directory. You can also create sub-directories to better organize your posts and assets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Markdown Example</title><link>http://blog.xiaban.run/posts/markdown/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/markdown/</guid><description>A simple example of a Markdown blog post.</description><pubDate>Sun, 01 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;An h1 header&lt;/h1&gt;
&lt;p&gt;Paragraphs are separated by a blank line.&lt;/p&gt;
&lt;p&gt;2nd paragraph. &lt;em&gt;Italic&lt;/em&gt;, &lt;strong&gt;bold&lt;/strong&gt;, and &lt;code&gt;monospace&lt;/code&gt;. Itemized lists
look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this one&lt;/li&gt;
&lt;li&gt;that one&lt;/li&gt;
&lt;li&gt;the other one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Block quotes are
written like so.&lt;/p&gt;
&lt;p&gt;They can span multiple paragraphs,
if you like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., &quot;it&apos;s all
in chapters 12--14&quot;). Three dots ... will be converted to an ellipsis.
Unicode is supported. ☺&lt;/p&gt;
&lt;h2&gt;An h2 header&lt;/h2&gt;
&lt;p&gt;Here&apos;s a numbered list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;first item&lt;/li&gt;
&lt;li&gt;second item&lt;/li&gt;
&lt;li&gt;third item&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here&apos;s a code sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define foobar() {
    print &quot;Welcome to flavor country!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(which makes copying &amp;amp; pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
# Quick, count to ten!
for i in range(10):
    # (but not *too* quick)
    time.sleep(0.5)
    print i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;An h3 header&lt;/h3&gt;
&lt;p&gt;Now a nested list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, get these ingredients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;carrots&lt;/li&gt;
&lt;li&gt;celery&lt;/li&gt;
&lt;li&gt;lentils&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boil some water.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dump everything in the pot and follow
this algorithm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; find wooden spoon
 uncover pot
 stir
 cover pot
 balance wooden spoon precariously on pot handle
 wait 10 minutes
 goto first step (or shut off burner when done)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not bump wooden spoon or it will fall.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).&lt;/p&gt;
&lt;p&gt;Here&apos;s a link to &lt;a href=&quot;http://foo.bar&quot;&gt;a website&lt;/a&gt;, to a &lt;a href=&quot;local-doc.html&quot;&gt;local
doc&lt;/a&gt;, and to a &lt;a href=&quot;#an-h2-header&quot;&gt;section heading in the current
doc&lt;/a&gt;. Here&apos;s a footnote [^1].&lt;/p&gt;
&lt;p&gt;[^1]: Footnote text goes here.&lt;/p&gt;
&lt;p&gt;Tables can look like this:&lt;/p&gt;
&lt;p&gt;size material color&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;9 leather brown
10 hemp canvas natural
11 glass transparent&lt;/p&gt;
&lt;p&gt;Table: Shoes, their sizes, and what they&apos;re made of&lt;/p&gt;
&lt;p&gt;(The above is the caption for the table.) Pandoc also supports
multi-line tables:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;keyword text&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;red Sunsets, apples, and
other red or reddish
things.&lt;/p&gt;
&lt;p&gt;green Leaves, grass, frogs
and other things it&apos;s
not easy being.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A horizontal rule follows.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Here&apos;s a definition list:&lt;/p&gt;
&lt;p&gt;apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There&apos;s no &quot;e&quot; in tomatoe.&lt;/p&gt;
&lt;p&gt;Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)&lt;/p&gt;
&lt;p&gt;Here&apos;s a &quot;line block&quot;:&lt;/p&gt;
&lt;p&gt;| Line one
| Line too
| Line tree&lt;/p&gt;
&lt;p&gt;and images can be specified like so:&lt;/p&gt;
&lt;p&gt;Inline math equations go in like so: $\omega = d\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:&lt;/p&gt;
&lt;p&gt;$$I = \int \rho R^{2} dV$$&lt;/p&gt;
&lt;p&gt;$$
\begin{equation*}
\pi
=3.1415926535
;8979323846;2643383279;5028841971;6939937510;5820974944
;5923078164;0628620899;8628034825;3421170679;\ldots
\end{equation*}
$$&lt;/p&gt;
&lt;p&gt;And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: `foo`, *bar*, etc.&lt;/p&gt;
</content:encoded></item><item><title>Include Video in the Posts</title><link>http://blog.xiaban.run/posts/video/</link><guid isPermaLink="true">http://blog.xiaban.run/posts/video/</guid><description>This post demonstrates how to include embedded video in a blog post.</description><pubDate>Tue, 01 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Include Video in the Post
published: 2023-10-19
// ...
---

&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;YouTube&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Bilibili&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item></channel></rss>