<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Docker Compose on Veno's Blog</title><link>https://v3n0.top/tags/docker-compose/</link><description>Recent content in Docker Compose on Veno's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Thu, 28 May 2026 17:21:56 +0800</lastBuildDate><atom:link href="https://v3n0.top/tags/docker-compose/index.xml" rel="self" type="application/rss+xml"/><item><title>从零开始搭建一个 Git 管理的 AstrBot 配置仓库</title><link>https://v3n0.top/post/2026/astrbot-git-managed-config/</link><pubDate>Thu, 28 May 2026 17:21:56 +0800</pubDate><guid>https://v3n0.top/post/2026/astrbot-git-managed-config/</guid><description>&lt;p>最近想把 AstrBot 当成一个长期可维护的团队机器人入口：后续它要接飞书、Gitea、沙箱、&lt;code>lark-cli&lt;/code>、&lt;code>tea&lt;/code>、自定义插件和 Skills。如果这些东西都只堆在某台机器的 &lt;code>data/&lt;/code> 目录里，迟早会变成不可复现的手工状态。&lt;/p>
&lt;p>所以第一步不是立刻做复杂自动化，而是先从 0 开始做一个可以被 Git 管理的 AstrBot 配置项目。本文先记录最小版本：创建独立仓库，用 Docker Compose 跑通 AstrBot WebUI，并明确哪些文件应该进 Git，哪些运行态文件应该忽略。&lt;/p>
&lt;h2 id="目标">目标
&lt;/h2>&lt;p>我希望最终得到这样的结构：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">astrbot-config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> .env.example
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> .gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> README.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> data/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> cmd_config.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mcp_server.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> plugins/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> skills/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ops/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里的重点是：&lt;code>data/&lt;/code> 不是整体入库，而是只把可复现、可审查的配置放进 Git。&lt;/p>
&lt;p>应该进入 Git 的内容包括：&lt;/p>
&lt;ul>
&lt;li>&lt;code>data/cmd_config.json&lt;/code>&lt;/li>
&lt;li>&lt;code>data/config/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/plugins/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/skills/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/mcp_server.json&lt;/code>&lt;/li>
&lt;li>自己维护的插件、Skills、配置模板&lt;/li>
&lt;/ul>
&lt;p>不应该进入 Git 的内容包括：&lt;/p>
&lt;ul>
&lt;li>&lt;code>.env&lt;/code>&lt;/li>
&lt;li>&lt;code>data/dist/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/dashboard.zip&lt;/code>&lt;/li>
&lt;li>&lt;code>data/*.db&lt;/code>&lt;/li>
&lt;li>&lt;code>data/knowledge_base/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/temp/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/workspaces/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/attachments/&lt;/code>&lt;/li>
&lt;li>&lt;code>data/site-packages/&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="初始化项目">初始化项目
&lt;/h2>&lt;p>新项目放在：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">/home/veno/Projects/astrbot-config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>先创建目录和 Git 仓库：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/Projects
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir astrbot-config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> astrbot-config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git branch -m main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>目录骨架：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p data/config data/plugins data/skills ops
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">touch data/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/config/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/plugins/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/skills/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> ops/.gitkeep
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="docker-compose">Docker Compose
&lt;/h2>&lt;p>&lt;code>compose.yml&lt;/code> 使用官方镜像：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">astrbot&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${ASTRBOT_IMAGE:-soulter/astrbot:latest}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${ASTRBOT_CONTAINER_NAME:-astrbot-config}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unless-stopped&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">security_opt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="kc">no&lt;/span>-&lt;span class="l">new-privileges:true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;${ASTRBOT_WEB_PORT:-6185}:6185&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;${ASTRBOT_ONEBOT_PORT:-6199}:6199&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">TZ=${TZ:-Asia/Shanghai}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./data:/AstrBot/data:z&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/etc/localtime:/etc/localtime:ro&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>.env.example&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ASTRBOT_IMAGE=soulter/astrbot:latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ASTRBOT_CONTAINER_NAME=astrbot-config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ASTRBOT_WEB_PORT=6185
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ASTRBOT_ONEBOT_PORT=6199
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">TZ=Asia/Shanghai
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>实际运行时复制一份 &lt;code>.env&lt;/code>，但 &lt;code>.env&lt;/code> 不进 Git：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cp .env.example .env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="git-ignore">Git Ignore
&lt;/h2>&lt;p>&lt;code>.gitignore&lt;/code> 的核心是排除运行态文件：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/dist/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/temp/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/attachments/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/webchat/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/site-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/knowledge_base/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/workspaces/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/plugin_data/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/dashboard.zip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/*.db
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/*.db-*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/t2i_templates/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Keep config-managed areas tracked.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">!data/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">!data/config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">!data/plugins/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">!data/skills/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样 AstrBot 首次启动时生成的一堆数据库、WebUI 静态资源、知识库文件不会污染配置仓库。&lt;/p>
&lt;h2 id="清掉旧实例">清掉旧实例
&lt;/h2>&lt;p>因为这是一个从 0 开始的新部署，所以旧 AstrBot 容器直接作废。&lt;/p>
&lt;p>先查看容器：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps -a --format &lt;span class="s1">&amp;#39;{{.ID}} {{.Names}} {{.Image}} {{.Ports}} {{.Status}}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当时发现旧容器 &lt;code>astrbot&lt;/code> 占用了 &lt;code>6185&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">astrbot soulter/astrbot:latest 0.0.0.0:6185-&amp;gt;6185/tcp Up ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>移除旧 AstrBot：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker rm -f astrbot
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>我没有动 &lt;code>napcat&lt;/code>，因为它不是 AstrBot 本体。&lt;/p>
&lt;h2 id="启动新实例">启动新实例
&lt;/h2>&lt;p>启动：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>第一次启动时踩到一个小坑：因为前一次端口冲突已经创建了容器，后续虽然配置正确，但实际端口没有发布出来。解决方式是强制重建：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose up -d --force-recreate
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>查看状态：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker ps --format &lt;span class="s1">&amp;#39;{{.Names}} {{.Ports}} {{.Status}}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>最终看到：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">astrbot-config 0.0.0.0:6185-&amp;gt;6185/tcp, 0.0.0.0:6199-&amp;gt;6199/tcp Up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>日志里确认 WebUI ready：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">AstrBot v4.24.2 WebUI is ready
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Local: http://localhost:6185
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Default username/password: astrbot / astrbot
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>访问地址：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">http://localhost:6185
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>默认账号密码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">astrbot / astrbot
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="处理生成的配置">处理生成的配置
&lt;/h2>&lt;p>AstrBot 会在 &lt;code>data/&lt;/code> 下生成主配置：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">data/cmd_config.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data/mcp_server.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>其中 &lt;code>data/cmd_config.json&lt;/code> 里会出现运行时生成的 &lt;code>dashboard.jwt_secret&lt;/code>。这个值不适合进 Git，所以提交前我把它清空：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;dashboard&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;jwt_secret&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>容器以 root 写入挂载目录时，本机用户可能无法直接修改文件。可以通过容器把 &lt;code>/AstrBot/data&lt;/code> 的所有权改回来：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> astrbot-config chown -R 1000:1000 /AstrBot/data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后再清理 &lt;code>cmd_config.json&lt;/code>。&lt;/p>
&lt;h2 id="初始提交">初始提交
&lt;/h2>&lt;p>确认 Git 状态：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git status --short --ignored
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>应该只看到 &lt;code>.env&lt;/code>、数据库、WebUI 静态资源等被忽略，配置文件和骨架目录可提交。&lt;/p>
&lt;p>提交：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add .env.example .gitignore README.md compose.yml &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/config/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/plugins/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/skills/.gitkeep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/cmd_config.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> data/mcp_server.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> ops/.gitkeep
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;chore: initialize astrbot docker config repo&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当前提交：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">8a4a00d chore: initialize astrbot docker config repo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="webui-初始配置">WebUI 初始配置
&lt;/h2>&lt;p>启动跑通之后，我在 AstrBot WebUI 里做了第一次人工配置：&lt;/p>
&lt;ul>
&lt;li>修改了初始 Dashboard 用户名和密码&lt;/li>
&lt;li>在模型提供商里创建了 DeepSeek API 来源&lt;/li>
&lt;li>启用了 &lt;code>deepseek/deepseek-v4-pro&lt;/code> 作为模型&lt;/li>
&lt;li>新建了一个配置文件，展示名称为 &lt;code>Feishu - Flow Project&lt;/code>&lt;/li>
&lt;li>暂时还没有接入飞书机器人、沙箱、CLI、Skills 或插件&lt;/li>
&lt;/ul>
&lt;p>这些操作反映到仓库里后，&lt;code>git status&lt;/code> 看到：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"> M data/cmd_config.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">?? data/config/abconf_99578339-b87b-41b4-97a9-5c4af491f6c2.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">?? data/skills.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>data/cmd_config.json&lt;/code> 的主要变化是：&lt;/p>
&lt;ul>
&lt;li>&lt;code>dashboard.username&lt;/code> 从 &lt;code>astrbot&lt;/code> 改成了 &lt;code>veno&lt;/code>&lt;/li>
&lt;li>&lt;code>dashboard.password&lt;/code> 更新为新的密码哈希&lt;/li>
&lt;li>&lt;code>dashboard.jwt_secret&lt;/code> 被运行时重新生成&lt;/li>
&lt;li>新增 &lt;code>provider_sources[0]&lt;/code>，来源 ID 为 &lt;code>deepseek&lt;/code>&lt;/li>
&lt;li>新增 &lt;code>provider[0]&lt;/code>，模型 ID 为 &lt;code>deepseek/deepseek-v4-pro&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>其中 DeepSeek API key 目前由 AstrBot WebUI 直接写进了 &lt;code>data/cmd_config.json&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;provider_sources&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;provider&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;deepseek&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;openai_chat_completion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;api_base&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://api.deepseek.com/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;deepseek&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;enable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里我最后选择了更朴素的方案：把这个仓库明确定位为私有配置仓库，并让 &lt;code>data/cmd_config.json&lt;/code> 继续作为 AstrBot 运行时事实来源进入 Git。&lt;/p>
&lt;p>原因是 AstrBot WebUI 会直接写这个 JSON，当前还不能确认它是否稳定支持从 &lt;code>.env&lt;/code> 或模板安全回填 provider key、Dashboard 密码哈希和 &lt;code>jwt_secret&lt;/code>。如果强行做模板渲染，反而可能引入“配置看起来可复现，但实际启动时被覆盖或漏注入”的风险。&lt;/p>
&lt;p>因此现阶段的安全边界是：&lt;/p>
&lt;ul>
&lt;li>私有 Git 仓库可以包含真实运行配置&lt;/li>
&lt;li>不配置公开 remote，不把配置 diff 贴到公开聊天或博客里&lt;/li>
&lt;li>如果未来要公开仓库，先轮换 provider key 和 Dashboard 凭据，再清理 Git 历史&lt;/li>
&lt;li>真要提高 secret 安全等级时，再引入 SOPS 或 git-crypt，而不是先做脆弱的自动回填&lt;/li>
&lt;/ul>
&lt;p>另外，新建的配置文件实际落在：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">data/config/abconf_99578339-b87b-41b4-97a9-5c4af491f6c2.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个 JSON 文件本身没有直接写入 &lt;code>Feishu - Flow Project&lt;/code> 字符串。也就是说，AstrBot 对“配置文件展示名”和 &lt;code>abconf_*.json&lt;/code> 文件之间的映射，可能不完全靠这个 JSON 文件表达。后续如果要做严格 GitOps，需要继续确认配置文件命名和展示名的持久化位置。&lt;/p>
&lt;h2 id="私有配置提交">私有配置提交
&lt;/h2>&lt;p>确认采用私有仓库策略后，我把 WebUI 产生的真实配置提交进 Git：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">e75b5fd chore: track astrbot webui config privately
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这次提交包含：&lt;/p>
&lt;ul>
&lt;li>&lt;code>data/cmd_config.json&lt;/code>：Dashboard 用户、DeepSeek 来源和启用模型等真实运行配置&lt;/li>
&lt;li>&lt;code>data/config/abconf_99578339-b87b-41b4-97a9-5c4af491f6c2.json&lt;/code>：WebUI 新建配置文件对应的 JSON&lt;/li>
&lt;li>&lt;code>data/skills.json&lt;/code>：当前为空的 Skills 注册状态&lt;/li>
&lt;li>&lt;code>README.md&lt;/code>：补充私有仓库安全边界说明&lt;/li>
&lt;/ul>
&lt;p>提交后工作区是干净的，并且当前没有配置 Git remote。&lt;/p>
&lt;h2 id="shipyard-neo-沙箱与-feishu-cli">Shipyard Neo 沙箱与 Feishu CLI
&lt;/h2>&lt;p>下一步我按 AstrBot 文档接入沙箱能力。文档里的关键点是：&lt;code>Computer Use Runtime&lt;/code> 可以设为 &lt;code>sandbox&lt;/code>，沙箱驱动选择 &lt;code>Shipyard Neo&lt;/code>；Bay 默认监听 &lt;code>8114&lt;/code>，AstrBot 通过 &lt;code>Shipyard Neo API Endpoint&lt;/code> 和 &lt;code>Shipyard Neo Access Token&lt;/code> 连接。&lt;/p>
&lt;p>这次采用本机独立 Shipyard Neo 部署，而不是把 Bay 塞进 AstrBot 容器：&lt;/p>
&lt;ul>
&lt;li>Shipyard Neo 部署目录：&lt;code>/home/veno/Projects/shipyard-neo-deploy&lt;/code>&lt;/li>
&lt;li>Bay endpoint：&lt;code>http://bay:8114&lt;/code>，供 AstrBot 通过 Docker 网络访问&lt;/li>
&lt;li>AstrBot &lt;code>computer_use_runtime&lt;/code>：&lt;code>sandbox&lt;/code>&lt;/li>
&lt;li>AstrBot &lt;code>sandbox.booter&lt;/code>：&lt;code>shipyard_neo&lt;/code>&lt;/li>
&lt;li>AstrBot &lt;code>shipyard_neo_profile&lt;/code>：&lt;code>python-lark&lt;/code>&lt;/li>
&lt;li>Bay token：按文档写入 &lt;code>shipyard_neo_access_token&lt;/code>，继续由私有配置仓库管理&lt;/li>
&lt;/ul>
&lt;p>Fedora 上还遇到一个实际问题：Bay 需要访问 Docker socket 来创建 sandbox 容器，但 SELinux 会拦截默认 bind mount。由于 Bay 挂 Docker socket 本身就已经是高权限组件，我在 Bay service 上显式加了：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">security_opt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">label:disable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这不是为了让普通业务容器放宽隔离，而是为了让 Bay 这个 Docker 控制面组件在 Fedora 上能按设计工作。&lt;/p>
&lt;p>Feishu CLI 和 Skills 按 &lt;code>larksuite/cli&lt;/code> 官方仓库处理：&lt;/p>
&lt;ul>
&lt;li>本机先执行官方推荐的 &lt;code>npx skills add larksuite/cli -g -y&lt;/code>，安装官方 lark skills&lt;/li>
&lt;li>AstrBot 仓库中同步了官方安装器发现的 26 个 lark skills 到 &lt;code>data/skills/&lt;/code>&lt;/li>
&lt;li>自定义 Shipyard Neo ship 镜像继承 &lt;code>ghcr.io/astrbotdevs/shipyard-neo-ship:latest&lt;/code>&lt;/li>
&lt;li>镜像内执行官方推荐的 &lt;code>npx /cli install&lt;/code> 安装 &lt;code>lark-cli&lt;/code>&lt;/li>
&lt;li>额外加入本机已有的静态 &lt;code>tea&lt;/code> 二进制&lt;/li>
&lt;/ul>
&lt;p>最终验证：Bay 能创建 &lt;code>python-lark&lt;/code> sandbox，并且在真实 sandbox 内执行：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">lark-cli --version &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> tea --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>返回 &lt;code>lark-cli version 1.0.42&lt;/code> 和 &lt;code>tea 0.12.0&lt;/code>。&lt;/p>
&lt;h2 id="cli-授权状态持久化">CLI 授权状态持久化
&lt;/h2>&lt;p>安装 &lt;code>lark-cli&lt;/code> 和 &lt;code>tea&lt;/code> 之后，还有一个更实际的问题：Agent 每次要用飞书或 Gitea 能力时，不应该重新思考“去哪登录、token 放哪、下次还在不在”。这件事不能交给 Skill 本身解决。Skill 只应该描述工具怎么用，真正的权限边界仍然应该在飞书应用权限、用户授权、Gitea token scope 和 sandbox 运行时配置上。&lt;/p>
&lt;p>一开始我以为只要把 &lt;code>HOME&lt;/code> 和 &lt;code>XDG_CONFIG_HOME&lt;/code> 指到 &lt;code>/workspace&lt;/code>，就能靠 Shipyard Neo Cargo 保存 CLI 登录态。后面继续查文档和实际验证后发现，这个判断只对了一半：managed cargo 会随着 sandbox 删除而释放，不能作为长期账号凭证存储。真正适合保存 CLI 凭证的是 external cargo。&lt;/p>
&lt;p>最终部署改成了这样：&lt;/p>
&lt;ul>
&lt;li>镜像层只安装工具：&lt;code>lark-cli&lt;/code>、&lt;code>tea&lt;/code> 和对应 Skills&lt;/li>
&lt;li>Skill 层只提供调用说明，不写入任何 token&lt;/li>
&lt;li>Shipyard Neo 增加显式 profile：&lt;code>python-lark&lt;/code>&lt;/li>
&lt;li>&lt;code>python-lark&lt;/code> 继续使用同一份工具镜像，并设置 &lt;code>HOME=/workspace&lt;/code>、&lt;code>XDG_CONFIG_HOME=/workspace/.config&lt;/code>&lt;/li>
&lt;li>Bay 中创建一个 external cargo，专门持久化 &lt;code>/workspace&lt;/code>&lt;/li>
&lt;li>AstrBot 创建 sandbox 时传入固定 &lt;code>cargo_id&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>对应的 Shipyard Neo profile 是：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python-lark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Python sandbox with lark-cli and tea, using persistent CLI config&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;shipyard-neo-ship-lark:latest&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime_type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ship&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime_port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8123&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">filesystem&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">shell&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">HOME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/workspace&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">XDG_CONFIG_HOME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/workspace/.config&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这里一开始还有一个 AstrBot 侧的小补丁：当前 AstrBot 的 &lt;code>ShipyardNeoBooter&lt;/code> 只传 &lt;code>profile&lt;/code> 和 &lt;code>ttl&lt;/code>，不会传 &lt;code>cargo_id&lt;/code>。后来证明这个方向不稳，因为 WebUI 更新 AstrBot 时会覆盖运行时源码。最终方案改为 Bay 侧 &lt;code>profile -&amp;gt; cargo&lt;/code> 映射：AstrBot 仍然只传 &lt;code>profile=python-lark&lt;/code>，Bay 在创建 sandbox 时自动补上对应的 external cargo。&lt;/p>
&lt;p>这个方案的边界比较清楚：&lt;/p>
&lt;ul>
&lt;li>token 不进镜像&lt;/li>
&lt;li>token 不进 Skill&lt;/li>
&lt;li>token 不出现在博客或公开日志里&lt;/li>
&lt;li>CLI 凭证落在 external cargo 的 &lt;code>/workspace/.config&lt;/code>&lt;/li>
&lt;li>AstrBot 容器不再改源码，Git 只记录 Bay 侧配置和可复现的部署代码&lt;/li>
&lt;/ul>
&lt;p>持久化验证也做过一次：在 external cargo 里写入测试文件，删除 sandbox 后重新创建，文件仍然存在。这说明凭证目录不再依赖某个临时 sandbox 生命周期。&lt;/p>
&lt;p>随后完成了两个 CLI 的登录和验证：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">lark_configured=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lark_auth_status_ok=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lark_auth_list_ok=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tea_authenticated=true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>其中 &lt;code>tea&lt;/code> 已能访问目标 Gitea 实例，账号验证为 &lt;code>veno&lt;/code>。Lark 侧使用脱敏验证方式，只检查 &lt;code>config show&lt;/code>、&lt;code>auth status&lt;/code>、&lt;code>auth list&lt;/code> 的退出码，不打印任何 app secret、access token 或用户授权内容。&lt;/p>
&lt;p>到这里，AstrBot 在后续调用 &lt;code>lark-cli&lt;/code> 和 &lt;code>tea&lt;/code> 时，不需要每次重新设计授权流程。模型只需要按 Skill 使用命令；具体能读写什么，由飞书服务端权限、用户授权范围、Gitea token scope 和 external cargo 中的当前登录态共同决定。&lt;/p>
&lt;h2 id="webui-更新与源码-patch">WebUI 更新与源码 Patch
&lt;/h2>&lt;p>这里后来又踩到一个和 WebUI 更新有关的坑。&lt;/p>
&lt;p>最初为了让 AstrBot 读取 &lt;code>ASTRBOT_SHIPYARD_NEO_CARGO_ID&lt;/code>，我把两个 patch 文件直接以只读 bind mount 的方式挂到了容器里的源码路径：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/AstrBot/astrbot/core/computer/booters/shipyard_neo.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/AstrBot/astrbot/core/computer/computer_client.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样运行时没问题，但从 WebUI 更新 AstrBot 时会失败：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">[Errno 30] Read-only file system: &amp;#39;/AstrBot/astrbot/core/computer&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>原因很直接：WebUI 更新器会尝试覆盖 AstrBot 本体源码，而源码目录下有只读挂载点。即使只挂了两个文件，更新过程遇到这些路径也会被文件系统拒绝。&lt;/p>
&lt;p>第一版修复方式是不要把 patch 文件直接挂到源码路径，而是挂到独立目录，并在容器启动时复制进去：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cp /AstrBot/local-patches/astrbot/core/computer/booters/shipyard_neo.py /AstrBot/astrbot/core/computer/booters/shipyard_neo.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cp /AstrBot/local-patches/astrbot/core/computer/computer_client.py /AstrBot/astrbot/core/computer/computer_client.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exec python main.py&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./data:/AstrBot/data:z&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./patches/astrbot:/AstrBot/local-patches/astrbot:ro,z&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样 patch 来源仍然是只读的、可 Git 管理的，但 &lt;code>/AstrBot/astrbot/core/computer&lt;/code> 本身恢复为容器内可写目录。WebUI 更新 AstrBot 时不会再被 bind mount 卡住；容器重启后也会重新应用本地 patch。&lt;/p>
&lt;p>修复后验证：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">computer_dir_writable=true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>但这个方案后面又暴露了第二层问题：WebUI 更新 AstrBot 之后，源码文件会被更新器重新覆盖。容器没有重建时，&lt;code>command&lt;/code> 里的复制动作不会再次执行，于是运行中的 AstrBot 又退回了“不传 &lt;code>cargo_id&lt;/code>”的原版逻辑。&lt;/p>
&lt;p>现象很典型：机器人调用 &lt;code>lark-cli&lt;/code> 时又提示未配置，甚至重新发起 &lt;code>lark-cli config init&lt;/code>。这并不是 external cargo 里的凭证丢了，而是 AstrBot 创建 sandbox 时没有再把 fixed cargo 传给 Bay，结果模型落进了新的 managed cargo。&lt;/p>
&lt;p>第二版改成了启动时最小文本 patch，并启动后台 watcher 周期性补打 patch。它能止血，但本质上还是在 AstrBot 容器里改源码：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> python /AstrBot/local-patches/apply-astrbot-cargo-patch.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> while sleep 10; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> python /AstrBot/local-patches/apply-astrbot-cargo-patch.py || true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ) &amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exec python main.py&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./data:/AstrBot/data:z&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./patches:/AstrBot/local-patches:ro,z&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个脚本只做三件事：&lt;/p>
&lt;ul>
&lt;li>给 &lt;code>ShipyardNeoBooter.__init__&lt;/code> 增加 &lt;code>cargo_id&lt;/code>&lt;/li>
&lt;li>调用 &lt;code>BayClient.create_sandbox()&lt;/code> 时传入 &lt;code>cargo_id&lt;/code>&lt;/li>
&lt;li>在 &lt;code>computer_client.py&lt;/code> 中从 &lt;code>shipyard_neo_cargo_id&lt;/code> 或 &lt;code>ASTRBOT_SHIPYARD_NEO_CARGO_ID&lt;/code> 读取 cargo&lt;/li>
&lt;/ul>
&lt;p>它比整文件覆盖更适合 WebUI 更新场景，但仍然不适合作为长期方案。真正的问题不是 AstrBot 是否能读某个环境变量，而是“创建 sandbox 时该由谁决定 cargo”。这个决定更应该放在 Bay 这个 sandbox 控制面里。&lt;/p>
&lt;p>最终我把方案改到了 Bay 侧：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">cargo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">root_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/var/lib/bay/cargos&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default_size_limit_mb&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1024&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mount_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/workspace&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">profile_defaults&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">python-lark&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ws-fd07bebac9ae&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Bay 的逻辑变成：&lt;/p>
&lt;ul>
&lt;li>如果请求显式带了 &lt;code>cargo_id&lt;/code>，尊重请求&lt;/li>
&lt;li>如果请求没带 &lt;code>cargo_id&lt;/code>，先按 &lt;code>profile&lt;/code> 查 &lt;code>cargo.profile_defaults&lt;/code>&lt;/li>
&lt;li>命中默认 cargo 时，用该 external cargo 创建 sandbox&lt;/li>
&lt;li>绑定 fixed cargo 的 profile 跳过 warm pool，因为 warm sandbox 自带 managed cargo，不能重绑&lt;/li>
&lt;/ul>
&lt;p>于是 AstrBot 侧可以恢复成原生容器：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">TZ=${TZ:-Asia/Shanghai}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./data:/AstrBot/data:z&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/etc/localtime:/etc/localtime:ro&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这次验证结论：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">bay_default_cargo_applied=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lark_auth_status_ok=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lark_auth_list_ok=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tea_authenticated=true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>所以问题不是凭证持久化无效，而是原来的 patch 生命周期不够强。fixed external cargo 仍然有效；现在由 Bay 根据 &lt;code>profile&lt;/code> 稳定选择 cargo，不再依赖 AstrBot 源码 patch。&lt;/p>
&lt;h2 id="不同配置文件使用不同账号">不同配置文件使用不同账号
&lt;/h2>&lt;p>接下来还有一个更重要的设计问题：如果不同 AstrBot 配置文件要使用不同的 Gitea 账号和飞书账号，账号状态应该绑在哪里？&lt;/p>
&lt;p>当前 AstrBot 配置文件可以各自设置 &lt;code>shipyard_neo_profile&lt;/code>，所以短期可以做多个 Shipyard Neo profile，例如 &lt;code>python-lark&lt;/code>、&lt;code>python-feishu-flow&lt;/code>、&lt;code>python-gitea-work&lt;/code>。每个 profile 可以复用同一份工具镜像，但在 Bay 的 &lt;code>cargo.profile_defaults&lt;/code> 里绑定不同 external cargo。这样每个账号组都有自己的 &lt;code>/workspace/.config&lt;/code>，CLI 配置不会互相踩。&lt;/p>
&lt;p>更干净的长期方案是把账号组作为部署资源管理：&lt;/p>
&lt;ul>
&lt;li>在 Shipyard Neo 中为每个账号组创建 external cargo&lt;/li>
&lt;li>在 AstrBot 配置文件里用 &lt;code>shipyard_neo_profile&lt;/code> 表达账号组&lt;/li>
&lt;li>由 Bay 根据 profile 选择对应 &lt;code>cargo_id&lt;/code>&lt;/li>
&lt;li>创建 sandbox 时传入 &lt;code>cargo_id&lt;/code>&lt;/li>
&lt;li>&lt;code>lark-cli&lt;/code> 和 &lt;code>tea&lt;/code> 继续把配置写到 &lt;code>/workspace/.config&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>这样账号状态就从“某个临时 sandbox 的副产物”变成“某个 AstrBot 配置文件绑定的持久化 workspace”。例如：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">default config -&amp;gt; cargo-default -&amp;gt; /workspace/.config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">feishu-flow config -&amp;gt; cargo-feishu-flow -&amp;gt; /workspace/.config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gitea-work config -&amp;gt; cargo-gitea-work -&amp;gt; /workspace/.config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>现在已经有了 Bay 侧 &lt;code>profile -&amp;gt; cargo&lt;/code> 映射，所以单账号组持久化已经跑通。后续要支持多账号组，只需要继续增加 profile 和 external cargo 映射，而不是继续把所有账号都塞进同一个 cargo。&lt;/p>
&lt;p>如果不想继续改 AstrBot，最稳妥的替代方案仍然是为不同账号组拆成不同 AstrBot 实例：每个实例有自己的配置仓库、自己的 Bay profile 或 Bay 部署、自己的 CLI 登录态。这比较笨，但隔离边界清晰，出问题时也容易回滚。&lt;/p>
&lt;h2 id="当前结果">当前结果
&lt;/h2>&lt;p>现在我有了一个新的、干净的 AstrBot 配置仓库：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/home/veno/Projects/astrbot-config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>它已经能通过 Docker Compose 启动 AstrBot，并且接上了可持久化 CLI 凭证的 Shipyard Neo sandbox。&lt;/p>
&lt;p>下一步再继续往这个仓库里加：&lt;/p>
&lt;ul>
&lt;li>用 Git 管理的全局 Skills&lt;/li>
&lt;li>插件内置 Skills&lt;/li>
&lt;li>自研 Gitea / 飞书同步插件&lt;/li>
&lt;li>可同步的机器人记忆、规则和状态映射&lt;/li>
&lt;/ul>
&lt;p>第一阶段先到这里：先让 AstrBot 以一个可复现的 GitOps 形态跑起来，并让后续工具调用不再重复处理基础授权问题。&lt;/p></description></item></channel></rss>