Files
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

16 KiB
Raw Permalink Blame History

DEPLOY — 本部署说明(ai1net.com / 47.77.182.89)

用途:把"这套平台到底部署在哪、装了什么、改了什么、怎么重建/回滚"一次讲清,供后来人(或未来的我)在不翻聊天记录的前提下独立重建。 对应档案 19 §C9。最后更新:2026-09-19(域名迁至 ai1net.com)

1. 拓扑与入口

项 值
服务器 47.77.182.89(Alibaba Cloud Linux 8,阿里云)|SSH 别名 bt-server(端口 32022,root)
门户 / 用户实例 https://ai1net.com / https://<用户名>.ai1net.com(Cloudflare 代理,回源走 443)
旧域名 alotbuy.com(含 *.alotbuy.com、*.dsh.alotbuy.com)→ 301 到新域(2026-09-19 降为过渡装置;relay-direct.alotbuy.com 与 /dshs-relay、/dshs-overlay/bootstrap 仍透传)
面板 宝塔(站点 vhost 由面板管理,注意:面板保存会重写 vhost,可能抹掉自定义指令)
编排器 自研 dshs(Node/TS),systemd 单元 dshs,监听 127.0.0.1:3080
被托管运行时 官方 @deepseek-ai/[email protected](全局安装于 /usr/local/lib/node_modules/@deepseek-ai/dsh,平台不改其任何文件)

2. 目录布局

路径 说明
/opt/dshs 编排器源码仓库(也是 git 仓库;lib/ 被 .gitignore 忽略 → 需构建)
/var/lib/dshs dataRoot:dshs.db(better-sqlite3)、users/<uuid>/
<userRoot> = dataRoot/users/<uuid> 每用户根:ws/(工作区,用户可见)、home/(DSH_HOME:profiles/sessions/storages/skills)、tmp/(1777)
<profile> = <userRoot>/home/profiles/web 该用户的 dsh profile:node_modules/、cordis.patch.yml、package.json、pnpm-lock.yaml
/opt/dsh/ 运维脚本与备份:switch-domain-alotbuy.sh、backups/、artifacts/(插件 tgz)
/www/server/panel/vhost/nginx/ ai1net.com.conf(现役门户)、relay-direct.conf(443/TCP 兜底)、alotbuy.com.conf(2026-09-19 降级为 301)、dsh.alotbuy.com.conf(301)+ 多份 .bak-*
/etc/letsencrypt/live/{ai1net.com,alotbuy.com,dsh.alotbuy.com}/ 证书(新域 DNS-01 via /etc/cloudflare-ai1net.ini;旧域 via /etc/cloudflare.ini)

3. 环境变量(/etc/dshs.env)

键 作用
DSHS_PORT 编排器监听端口(127.0.0.1:3080)
DSHS_DATA_ROOT dataRoot
DSHS_BASE_DOMAIN 门户/用户子域基域(现 ai1net.com)
DSHS_COOKIE_DOMAIN 会话 Cookie 域(现 .ai1net.com;见档案 22 §五 取舍说明)
DSHS_SECURE_COOKIES Cookie Secure 标记(HTTPS 下应开)
DSHS_DSH_BIN 子实例可执行路径
DSHS_ISOLATION_MODE 隔离模式(本部署 account = bwrap + setpriv + systemd-run scope)
DSHS_BASE_UID 每用户 uid 起始值

其余可调项走代码默认值(崩溃退避/熔断、idle-reap、bundled skill dir 等),见 src/config.ts 的 DEFAULT_* 与档案 20。

4. 外部依赖(版本为实测)

依赖 版本 用途
Node 22.23.2 编排器与子实例运行时
pnpm 9.15.9 profile 侧插件安装账本(pnpm-lock.yaml)
bubblewrap 0.4.0 每用户沙箱(合成根 + 只读系统 + 私有 tmp + --unshare-pid)
systemd-run — per-instance scope(MemoryHigh=448M(基础/软限) + MemoryMax=1024M(上界/硬限),与插件开关无关(档案 96)/ CPUQuota=150% / TasksMax=128)
nginx 1.28.3(宝塔) 站点与反代(关键自定义指令见档案 21/22)
nftables — 出网护栏(/etc/nftables-dsh-egress.nft + dsh-egress.service)
sqlite3 CLI 3.26 仅运维排查(平台内置用 better-sqlite3)
certbot + dns-cloudflare — 证书签发/续期(必须 --dns-cloudflare-propagation-seconds 60,否则失败)

5. 平台侧脚本族

脚本 作用
ensure-role-profile-patch.cjs 角色化 profile patch(非 admin 隐藏模型分区/收归插件开关)
ensure-workspace-picker.cjs 目录选择器收敛全量/新用户铺开(写平台段 + 逐用户 pnpm add;幂等,--dry-run/--restart)
07-scripts/install-workspace-picker.sh <tgz> 逐用户安装插件包(自动判断是否加 pnpm -w)
/opt/dsh/switch-domain-alotbuy.sh 域名切换(改 env → drain → 重启)
/usr/local/bin/provision-new-users.sh 新用户供应(建 OS 账号 + chown + 调用 picker 铺开);由 dsh-provision.path 触发
07-scripts/ci.sh 类型检查 + 构建 + 单测(不含需要实例/凭据的 smoke)

6. 构建 / 部署 / 回滚(三步)

# 构建(改代码后必做;lib/ 不入库)
cd /opt/dshs && npm run build && bash 07-scripts/ci.sh

# 部署(drain: 停服务 → 清残留实例 → 起服务)
systemctl stop dshs; for p in $(ps -eo pid,user,args | awk '$2 ~ /^dsh-/ {print $1}'); do kill $p; done; systemctl start dshs

# 回滚(示例:回滚到某个提交)
git -C /opt/dshs revert <sha> && npm run build && systemctl restart dshs
  • 本机 ↔ 服务器同步:服务器无 Gitea 凭据 → 服务器 git bundle create → 本机 git fetch <bundle>(路径必须用 D:/…)→ merge --ff-only → push。
  • 文档库:bash 07-scripts/docs-sync-check.sh(双端 md5 对账,0 = 全绿);服务器侧 /opt/dsh/docs(root 600)。

6.4 数据库初始化(PostgreSQL 13 / 47 本机进程)

DB 不在容器里,是 47 上本机进程(127.0.0.1:15432)。重建时按序跑:

-- ① 建角色 + 库(初始形态)
CREATE ROLE dshs LOGIN PASSWORD '<见 config/platform.env>';
CREATE DATABASE dshs OWNER dshs ENCODING 'UTF8' TEMPLATE template0;

-- ② 🔴 插件建库权限(2026-09-23 · B 档拍板)
--    用途 = 平台侧为每个业务插件建独立库 `dshs_pl_<pluginId>`(一插件一库)
--    ⛔ 只给这一位;⛔ 不得顺手给 CREATEROLE / SUPERUSER
ALTER ROLE dshs CREATEDB;
  • 连接串只从 systemd drop-in dshs.service.d/cluster.conf 取(DSHS_DB_URL);⚠️ source /etc/dshs.env 会拿到错的 5432(见 CODEBUDDY.md §8)。
  • 回滚②:ALTER ROLE dshs NOCREATEDB; ⇒ 已建的 dshs_pl_* 库不受影响,仅新插件建不了库(平台建库口返回 PG_CREATEDB_MISSING / 503)。
  • ⚠️ 权限校验判据(重建后必跑):rolsuper=false · rolcreatedb=true · rolcreaterole=false —— 多点一位即为权限失控。
  • ⚠️ PG 没有"只允许 dshs_pl_* 前缀"的原生机制(实测建任意名库成功)⇒ 库名白名单只靠平台代码兜(点生成 + 正则复校 + 建库落台账)。详见 03-数据库/DB-03-插件数据面规范.md §六。
  • 迁移表由平台启动时 runPgMigrations 自动落(v15 起含台账表 plugin_datastores / plugin_data_audit)⇒ 部署新版本代码后随 systemctl restart dshs 自动补齐,⛔ 不需手工建表。

6.5 定时维护(cron,2026-09-11 建立)

任务 文件 时间
存储用量快照 /etc/cron.d/dsh-maintenance 每小时 :05
工作区清理 / 会话回收 / 回收站清空 同上 04:10 / 04:20 / 04:40
平台全量备份 + 旧备份清理 /etc/cron.d/dsh-backup 每周日 05:00 / 05:30

备份脚本 /opt/dsh/backup.sh(入库副本 07-scripts/backup-platform.sh)含 SQLite 一致性快照;恢复步骤见档案 02 附录 C.8。

6.6 跨节点内容分发(节点侧拉取配置 · 2026-09-25 落地)

共享只读插件层(/var/lib/dshs/bundled-plugins)是每机一份的实体。此前它的跨机同步是 手工的(tar -czf - | ssh … tar -xzf -)⇒ 节点上少一个包,表现是"那台机上的用户启用插件 撞 plugin_bundle_missing",而平台侧看不见这个差异。

形态(2026-09-24 用户拍板):节点主动拉取(pull)+ 主节点核对并把差异告知。 ⛔ 不是"平台把包推下去"(推的形态对 NAT 后的客户端节点不成立,且要新开节点入站口)。

谁 做什么
节点(worker,如 w-106) 每 30 min(可调)自报本机共享层实况 → 按 Manager 回的差异拉产物 → 解包后自算指纹自校验 → 原子换盘
Manager(47) 只读:出权威清单、把差异告知回去、按 flat 给产物字节;把每次对账落成对账面

节点侧启用(加到 worker 单元真正读的那个 env 源里;🔴 写错地方 = 静默不生效):

# 第 0 步:看这台机的 worker 单元读哪个 env 源(106 实测 = 只有 EnvironmentFile,**没有** drop-in 目录)
systemctl cat dshs-worker | grep -E 'EnvironmentFile|^Environment='

# 情形 A(106 现状,2026-09-25 落地的就是这个):
echo 'DSHS_SHARED_PULL_URL=https://ai1net.com' >> /etc/dshs-worker.env
# 情形 B:该机有 drop-in,且里面已有同名键(哪怕写成空 `Environment=DSHS_SHARED_PULL_URL=`)
#   ⇒ **drop-in 优先于 EnvironmentFile**,必须写在 drop-in 里,否则被静默压掉:
#   /etc/systemd/system/dshs-worker.service.d/cluster.conf 追加
#   Environment=DSHS_SHARED_PULL_URL=https://ai1net.com

systemctl restart dshs-worker        # 有 drop-in 的机器先 daemon-reload
# 🔴 改完必须核对真的进了进程(判据见下面 ⑤)—— "文件写了" ≠ "生效了"。

可选项(同为环境变量):DSHS_SHARED_PULL_INTERVAL_MS=1800000(默认 30 min)| DSHS_SHARED_PULL_DISABLE=1(临时停用,⛔ 不改 URL 本身)。 ⛔ 不要写 /etc/dshs.env —— 那是控制面(dshs)读的,worker 不读它。

  • 🔴 默认关:DSHS_SHARED_PULL_URL 为空 ⇒ 一个请求都不发(单机形态 / Manager 本机零影响)。
  • 🔴 两机共享层根必须同值(现为 /var/lib/dshs/bundled-plugins)—— link: 引用把绝对路径 写进了用户 profile,根一分叉老用户就悬空(见 src/supervisor/plugin-assembly.ts depRefOf 注释)。
  • ⚠️ 两机代码根不同名(47 /opt/dshs、106 /opt/dshs-cluster,106 上没有 /opt/dshs)—— 与本项无关(拉取目标是数据目录,不是代码目录),但部署产物时别搞混。

Manager 侧三只只读口(⛔ 都不是给人用的 ⇒ 不走 requireAuth/requireAdmin,只认节点身份):

方法 路径 作用
POST /api/plugins/shared/node/sync 入参 = 本机实况清单,回 = 权威清单 + diff{missing,stale,extra} + pull
GET /api/plugins/shared/node/pack/:flat 回某包的 tar.gz;响应头带权威 x-dsh-tree-sha256
POST /api/plugins/shared/node/result 节点回报本轮失败项(只为对账面)
  • 身份 = 每机密钥:请求头 x-dsh-agent-token: <该机 agent_token> + x-dsh-host-id: <dsh_hosts.id>。 两值都对才放行;缺失 / 不存在 / 不匹配一律 401 且不区分原因。
  • ⚠️ 这把密钥双向可用(Manager 拨节点用它、节点来拉也用它)⇒ 一台被攻陷的 worker 除"被管理"外还能读共享层产物(它那台机的用户本来就能开通那些包,且只读)。 要收窄 ⇒ 给节点另发一把只读拉取钥,⛔ 不是把本口改宽。
  • ⛔ pack/:flat 只接受清单里已有的 flat,其余一律 404 not_here(调用方给不了路径)。
  • ⚠️ not_here(这台机上现在没有该包)≠ "包是空的" —— 两者语义不同,节点侧也照着分开处理。

验收判据(命令 + 期望输出 + 退出码)

# ① 手动触发一轮(节点本地;与 /fence 同级、必须带 token)
#    TOK 从**单元实际读的 env 源**取(106 = /etc/dshs-worker.env;有 drop-in 的机器加上 drop-in 路径)
TOK=$(grep -hE '^DSHS_(CLUSTER_)?AGENT_TOKEN=' /etc/dshs-worker.env \
        /etc/systemd/system/dshs-worker.service.d/*.conf 2>/dev/null | tail -1 | cut -d= -f2-)
curl -s -XPOST -H "x-dsh-agent-token: $TOK" http://127.0.0.1:19100/shared-layer/pull
#    期望:{"ok":true,...,"pulled":[...]}(首次必定有 pulled;⇒ printf 出 rc=0)
#    ⛔ 别加 `-H "content-type: application/json"`:空 body + JSON 类型会被 Fastify 判 400
#       (`FST_ERR_CTP_EMPTY_JSON_BODY`)—— 看着像接口坏了,其实只是调用方式不对。
#    无 token 必须 401(缺省/不匹配一律 401 且不区分原因):
#      curl -s -o /dev/null -w '%{http_code}\n' -XPOST http://127.0.0.1:19100/shared-layer/pull

# ② 节点侧落盘证据
cat /var/lib/dshs/bundled-plugins/.node-pull.json      # 期望:每包一条 treeSha256
ls -la /var/lib/dshs/bundled-plugins | grep -c 'pull-stage'   # 期望:0(stage 必须清净)

# ③ 两机逐字比对(⛔ 逐机读数,别写"三机一致"这类没逐机核过的结论)
ssh bt-server 'node -e "…hashTree…"'   # 见 ④ 的对账面字段 treeSha256,两机同包必须同值

# ④ Manager 侧对账面(admin 面)
curl -s -b <admin-cookie> https://ai1net.com/api/plugins/shared | jq '.nodes'
#    期望:该 hostId 一条快照;missing/stale 全空 = 已对齐

回滚:删掉 drop-in 里那行 DSHS_SHARED_PULL_URL → daemon-reload → restart dshs-worker ⇒ 立刻回到"不拉取"状态(⛔ 已拉下来的产物不回滚,它们本来就是对的)。 手工兜底命令照旧可用: ssh bt-server 'tar -C /var/lib/dshs/bundled-plugins -czf - <flat>' | ssh test106 'tar -C /var/lib/dshs/bundled-plugins -xzf - && chown -R root:root /var/lib/dshs/bundled-plugins/<flat>'

# ⑤ 确认配置真进了进程(只看"文件写了"不算数)
PID=$(systemctl show -p MainPID --value dshs-worker)
tr '\0' '\n' < /proc/$PID/environ | grep SHARED_PULL
#    期望:DSHS_SHARED_PULL_URL=https://ai1net.com

⚠️ 对账面不是现算真值:它只是"节点上一次来拉取时"的快照 ⇒ 节点离线 / 刚发新版都会让它滞后。 要现算 ⇒ 先让节点跑一次 ①,再回读 ④。

已知现象(2026-09-25 实测 · 非缺陷):restart dshs-worker 后启动那一轮的 journal 可能只有 [shared-pull] 已启用:… + [shared-pull] 对账取不到(sync_unreachable: fetch failed)—— 本轮⛔ 未改动任何本地文件。 这是启动瞬间的瞬时失败(同一时刻同机用 curl / node fetch 打同一 URL 都正常,纯时序), 且"取不到对账就一个字节都不动"正是设计语义 ⇒ 无副作用;下一轮(≤ 30 min)自愈,要立即恢复就跑 ①。

落地实测(2026-09-25 · 第 14 棒):106(w-106)启用后,三个共享包 _dsh-local_storyforge(117 文件) / dsh-plugin-mcn-suite(657) / _dsh-local_im-conversation-tabs(5) 逐机实算指纹与 47 全等。其中 storyforge 此前在 106 上是内容漂移的 (106 8a19709c… vs 权威 1c45c841…,文件数同为 117 ⇒ 只有逐字节指纹才看得出), 启用本项后被自动纠正。第二轮拉取 missing/stale/extra 全空(收敛)。

7. 默认红线(改任何东西前先读)

  1. 不自动升级 dsh;升级必须走"测试 → 评估 → 修复",并按档案 26 回归清单逐项验。
  2. 不改官方 dsh 主程序与缓存(/usr/local/lib/node_modules/@deepseek-ai/dsh/** 保持原封)。
  3. client bundle 禁 exports.default(R3)。
  4. 未明确要求不擅自 commit/push(服务器 git 与文档 git 都一样)。
  5. 服务器 /opt/dsh/docs 保持 root 600。

相关档案:01(架构)|02(运维,含附录 C 速查)|21(nginx 性能/流式)|22(域名迁移)|26(升级耦合点与回归清单)