myxce/src/docs/chatbot-verification.md
lei.chen 4a4ae16725 docs(scheduler-agent): add Claude Code integration guide to verification manual
New §5 in src/docs/chatbot-verification.md: how to connect Claude Code to
the scheduler-agent MCP server. Covers building the mcp-server binary,
`claude mcp add` CLI registration (verified against installed CLI
v2.1.220), the .mcp.json project config schema, the three scopes
(local/user/project), the first-run approval gate, verification commands,
and stdio/env gotchas. Original §5-§7 renumbered to §6-§8.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02 04:26:23 +00:00

15 KiB
Raw Blame History

Chatbot (Scheduler-Agent) 集群部署与验证手册

版本:2026-08-01 分支:chatbot(myxce fork) 目标集群:t1 (172.31.1.30) / t2 (172.31.1.31) / t3 (172.31.1.32)

本文档记录 scheduler-agent(chatbot 分支)在真实测试集群上的部署与验证步骤,可完整复现本次会话的操作。验证覆盖 Phase 0(只读)+ Phase 1(写操作:submit),以及发现的 apiserver 预存 bug。


1. 架构总览

用户自然语言 → Claude Code → MCP stdio → mcp-server → schedulerclient → apiserver REST API → controller
                                  (读:6 只读工具 + 写:4 写工具)
组件 二进制 部署位置 端口
controller xce_controller t1(单节点,非 HA) 9990
apiserver xce_apiserver t1 8080
scheduler xce_scheduler t1 7890
worker xcecd t1 / t2 / t3 —

拓扑说明:本次验证采用 t1 单节点 controller(3 节点 HA 选举因遗留 term 状态卡在 STANDBY,见 §7 已知问题)。单节点用 --peers 省略即可。


2. 前置:构建 AppImage

AppImage 是部署的载体——/opt/newxce/ 目录 = AppImage 解包后的 store 树 + run.sh 调度器。

# 在本地仓库根目录
cd /home/lchen/code/xce/src

# 1. 构建 xce wrapper(含 xcecore 闭包)
nix build .#xce

# 2. 打包 AppImage(280MB,mksquashfs 单遍)
#    输入:新 xce 闭包(nix path-info -r)+ AppRun + entrypoint
#    详见 /home/lchen/xce-appimage/package.sh
./package.sh   # → /home/lchen/xce-appimage/xce-x86_64.AppImage

AppImage 结构(与旧版一致,验证 unsquashfs -offset 1002016 -l):

squashfs-root/AppRun
squashfs-root/entrypoint        # → 指向 xcecore 的 bin/xce
squashfs-root/mountroot
squashfs-root/nix/store/…       # 99 个 store 路径闭包

3. 部署到 t1/t2/t3

3.1 部署 AppImage 结构

三台主机均为 lchen 用户,SSH 免密(~/.ssh/id_rsa),有 passwordless sudo。

# t1 完整部署(store 树 + run.sh + config)
# /opt/newxce/ 结构:
#   bin/xce                # AppImage 二进制(280MB 静态 ELF)
#   app/nix/store/…        # 99 个 store 路径(xcecore + 依赖)
#   run.sh                 # daemon 调度器(controller/worker/scheduler/apiserver)
#   config/                # 集群 + apiserver 配置

# t2/t3 从 t1 同步 store 树(worker 需要 xcecd + 依赖闭包)
ssh t1 'rsync -a /opt/newxce/app/ t2:/opt/newxce/app/ && rsync -a /opt/newxce/run.sh t2:/opt/newxce/run.sh'
ssh t1 'rsync -a /opt/newxce/app/ t3:/opt/newxce/app/ && rsync -a /opt/newxce/run.sh t3:/opt/newxce/run.sh'

3.2 停旧 systemd 服务(可选,若存在旧 XCE)

ssh t1 'sudo systemctl stop scheduler.service workerd.service; sudo systemctl disable scheduler.service workerd.service'
ssh t2 'sudo systemctl stop workerd.service; sudo systemctl disable workerd.service'

3.3 准备 controller workdir + 证书 + cluster.bin

# 每台 controller 需要 workdir + TLS 证书(HA 复制用;单节点也生成以防切换)
for h in t1 t2 t3; do
  ssh $h "sudo mkdir -p /opt/xce/controller/conf /opt/xce/controller/data /opt/xce/controller/certs /opt/xce/logs \
    && sudo chown -R lchen:lchen /opt/xce/controller /opt/xce/logs \
    && openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
       -keyout /opt/xce/controller/certs/xce.key -out /opt/xce/controller/certs/xce.crt -subj '/CN=xce'"
done

# 生成 cluster.bin(t1 上,用 AppImage 内的 xceadmin)
ssh t1 '/opt/newxce/app/nix/store/<xcecore-hash>/bin/xceadmin config build /opt/newxce/config -o /tmp/cluster.bin'

# 部署到 t1 的 controller conf(指针文件 + 版本化 bin)
# tag = cluster.bin 的 sha256 前 12 位(上面命令 stderr 输出)
ssh t1 'sudo mkdir -p /opt/xce/controller/t1/conf \
  && sudo cp /tmp/cluster.bin /opt/xce/controller/t1/conf/cluster.bin.<tag> \
  && printf "cluster.bin.<tag>\n" | sudo tee /opt/xce/controller/t1/conf/current'

conf/current 指针机制:<workdir>/<myname>/conf/current 内容为 cluster.bin.<12hex>,实际配置在 cluster.bin.<12hex>。controller 通过 load_primary_config 读取。tag 是配置 SHA-256 前 12 位。

3.4 准备 apiserver 配置

# apiserver 需要 <topdir>/config/{app,auth,rbac,logger}.yaml
ssh t1 'sudo mkdir -p /opt/xce/apiserver/config \
  && cp /opt/newxce/config/app.yaml /opt/newxce/config/auth.yaml \
     /opt/newxce/config/rbac.yaml /opt/newxce/config/logger.yaml \
     /opt/xce/apiserver/config/'

# 注意:/opt/newxce/config/rbac.yaml 是旧版(无 mcp_operator)。
# 必须用本地仓库的 rbac.yaml(含 mcp_operator + queue:control)
scp src/frontend/apiserver/config/rbac.yaml t1:/opt/xce/apiserver/config/rbac.yaml

3.5 启动 daemon

所有 daemon 通过 /opt/newxce/run.sh 启动(设置 LD_LIBRARY_PATH + 正确的 glibc 加载器)。用 setsid … & 完全脱离 SSH 会话。

# t1: controller(单节点,省略 --peers = 非 HA)
ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh controller -n t1 \
  -w /opt/xce/controller --certdir /opt/xce/controller/certs \
  -l /opt/xce/logs/xce_controller.log --vcpus 4 -v </dev/null >/dev/null 2>&1 &"'

# t1: apiserver(backend 指向 localhost:9990,配置在 /opt/xce/apiserver)
ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh apiserver -d /opt/xce/apiserver </dev/null >/dev/null 2>&1 &"'

# t1: scheduler + worker
ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh scheduler --peers t1=127.0.0.1:9990 -l /opt/xce/logs/xce_scheduler.log -v </dev/null >/dev/null 2>&1 &"'
ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh worker -c /opt/xce/config/worker.yaml </dev/null >/dev/null 2>&1 &"'

# t2/t3: worker
ssh t2 'sudo bash -c "cd /opt/newxce && setsid ./run.sh worker -c /opt/xce/config/worker.yaml </dev/null >/dev/null 2>&1 &"'
ssh t3 'sudo bash -c "cd /opt/newxce && setsid ./run.sh worker -c /opt/xce/config/worker.yaml </dev/null >/dev/null 2>&1 &"'

启动要点:

  • 必须加 --vcpus 4(--vcpus 缺失会导致 vtask pool 未初始化,vtask unavailable (pool=nil),无法完成选举)
  • controller 以 root 运行(sudo)
  • setsid … & 使进程脱离会话,否则 SSH 会话会挂起
  • 检查进程用 sudo fuser 9990/tcp / sudo ps aux | grep ld-linux.*xce(进程名是 ld-linux-x86-64.so.2,不是 xce_controller)

3.6 启动验证

# 端口监听
ssh t1 'sudo ss -tlnp | grep -E ":9990|:8080|:7890"'

# controller 应为 CTRL_PRIMARY
ssh t1 'sudo grep -E "CTRL_PRIMARY|become_leader" /opt/xce/logs/xce_controller.log | tail'

# apiserver 响应
ssh t1 'curl -s http://localhost:8080/xce/v1/cluster'   # 应返回 401(需认证)而非连接失败

预期 controller 日志:

raft_cb_become_leader: → CTRL_PRIMARY (term=1, first_boot=1)
committed_lsn 0 -> 1

4. 验证 Chatbot(Phase 0-1)

4.1 登录 + 生成 mcp_operator API Key

# 1. 登录(auth.yaml 有 admin/admin, cluster_admin)
TOKEN=$(curl -s -X POST http://localhost:8080/xce/v1/auth/logon \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")

# 2. 生成 mcp_operator key(服务账号,供 mcp-server 用)
curl -s -X POST http://localhost:8080/xce/v1/apikeys \
  -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -d '{"username":"scheduler-agent","role_name":"mcp_operator","description":"phase1","expiry_days":365}'
# → 返回 xce_ak_<64hex>

4.2 Phase 0 — 只读验证

KEY="xce_ak_<生成的key>"
BASE="http://localhost:8080"

# 全部应返回 200
curl -s -H "Authorization: $KEY" "$BASE/xce/v1/queues"
curl -s -H "Authorization: $KEY" "$BASE/xce/v1/hosts"
curl -s -H "Authorization: $KEY" "$BASE/xce/v1/jobs"
curl -s -H "Authorization: $KEY" "$BASE/xce/v1/resources/definition"

4.3 Phase 1 — 写操作验证

# submit → 201,返回 jobid
JOB=$(curl -s -X POST -H "Authorization: $KEY" -H "Content-Type: application/json" \
  -d '{"command":"sleep 60","job_name":"verify","queue_name":"normal"}' \
  "$BASE/xce/v1/jobs/create" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['jobid'])")

# 作业应进入 controller(PEND)
curl -s -H "Authorization: $KEY" "$BASE/xce/v1/jobs"

当前限制:hold/release/cancel(job control)返回 HTTP 500 —— apiserver 预存 bug(bug-1490),见 §7。


5. 接入 Claude Code

让 Claude Code(终端里的 AI 助手)直接用自然语言操作集群。Claude Code 通过 MCP 协议连接我们的 mcp-server,后者再经 schedulerclient 调 apiserver。

Claude Code ──stdio/MCP──> mcp-server ──HTTP──> apiserver ──> controller
                          (我们的二进制)

5.1 构建 mcp-server 二进制

cd /home/lchen/code/xce/src/frontend/mcp-server
go build -o /tmp/xce-mcp ./cmd/main.go
# 产物 ~12MB,CGO_ENABLED=0 纯静态(生产可放到 /usr/local/bin/xce-mcp)

前置:集群必须已在跑(见 §3),且有 mcp_operator API Key(见 §4.1)。

5.2 方式 A:CLI 注册(推荐)

claude mcp add 会写配置并校验语法(Claude Code v2.x 验证过的格式):

claude mcp add xce-scheduler -s project \
  -e XCE_API_URL=http://172.31.1.30:8080 \
  -e XCE_API_KEY=<你的 mcp_operator key> \
  -- /tmp/xce-mcp
  • xce-scheduler:服务器名(Claude 看到的工具前缀)
  • -s project:写进项目根 .mcp.json(可随 git 共享给团队)。其他选项:local(默认,只在本机本项目)、user(本机所有项目)
  • -e:传给二进制两个环境变量(XCE_API_URL 是 apiserver 地址;XCE_API_KEY 必填,缺了 mcp-server 会 log.Fatal)
  • -- 之后是二进制路径 + 参数;-e/-s 等标志必须在 -- 之前
  • stdio 是默认传输,无需 -t stdio

5.3 方式 B:手写 .mcp.json

项目根 .mcp.json(当前是空的 {"mcpServers": {}})填成:

{
  "mcpServers": {
    "xce-scheduler": {
      "type": "stdio",
      "command": "/tmp/xce-mcp",
      "args": [],
      "env": {
        "XCE_API_URL": "http://172.31.1.30:8080",
        "XCE_API_KEY": "<你的 mcp_operator key>"
      }
    }
  }
}
  • "type": "stdio" 必须(本地子进程服务器);远程 HTTP 服务器用 "type": "http" + "url"
  • command 可以是绝对路径
  • env 支持 ${VAR} 插值——不要提交明文 key,推荐写成 "XCE_API_KEY": "${XCE_API_KEY}",启动 Claude Code 前 export XCE_API_KEY=...

5.4 验证接入

claude mcp list                      # 列出所有服务器,带健康检查
claude mcp get xce-scheduler         # 查看某服务器的详情(类型/命令/env/状态)
claude mcp remove xce-scheduler      # 移除
claude mcp reset-project-choices     # 重新触发审批
  • 项目级(.mcp.json)服务器首次需审批:claude mcp list 显示 ⏸ Pending approval,下次在项目里启动 claude 时接受即可。local/user 级无需审批。
  • 会话内可用 /mcp 命令查看/重连服务器,无需重启。
  • 审批通过后,Claude 就能用这些工具了。可以问:"查看集群里有哪些作业在排队"(走 scheduler_list_jobs)或 "帮我提交一个 sleep 60 的作业"(走 scheduler_submit,会先弹 review 卡确认)。

5.5 注意事项

  • stdio 约束:mcp-server 只能往 stdout 写 MCP 协议消息;任何 fmt.Println 到 stdout 会损坏连接。本项目安全——mcp-go 的 ServeStdio 用 stdout 走协议,main.go 的 log.Printf 写 stderr。改代码时保持这个约定。
  • XCE_API_URL 必须显式设:不设默认 https://localhost:8080,生产环境几乎肯定不对。
  • 写操作默认要确认:scheduler_submit 有 dry_run review 模式 + destructive 注解(Claude Code 会先弹确认);cancel/hold/release 也会弹确认。这是设计文档 §4.5 的"require confirm"在 MCP 层的体现。
  • 当前 cancel/hold/release 不可用:底层 apiserver control 链路有 bug(bug-1490,HTTP 500)。接入后 submit 可用,control 类工具会报错,直到 bug-1490 修复。

6. schedulerclient / mcp-server 本地验证

在本地仓库(chatbot 分支)可独立运行 Go 测试(不需要集群):

# schedulerclient(读 + 写)
cd src/frontend/schedulerclient && go vet ./... && go build ./... && go test ./... -count=1

# mcp-server(含模板 + handlers)
cd src/frontend/mcp-server && go vet ./... && go build ./... && go test ./... -count=1

# apiserver RBAC
cd src/frontend/apiserver && CGO_ENABLED=0 go test ./controllers/... -run 'RBAC|APIKey' -count=1

schedulerclient 用 API key 连真实集群(若集群在跑):

cd src/frontend/schedulerclient
cat > /tmp/demo.go <<'EOF'
package main

import (
	"context"
	"fmt"
	sclient "schedulerclient"
)

func main() {
	c := sclient.NewClient("http://172.31.1.30:8080", "xce_ak_<key>")
	qs, _ := c.ListQueues(context.Background())
	for _, q := range qs {
		fmt.Printf("queue %s status=%s run=%d pend=%d\n", q.Name, q.Status, q.RunningJobs, q.PendingJobs)
	}
}
EOF
go run /tmp/demo.go

7. 已知问题

问题 状态 Bug
job control (cancel/hold/release) HTTP 500 未修。请求未达 controller(controller 日志无 control 事件)。已修 job_status int64 断言(从 "Invalid job_status" 变为 500),500 本体待排查 bug-1490
3 节点 HA 选举死锁 遗留 term 状态(ha-term.bin 残留 voted_for=t1, term=1)+ standby 无 pool 导致所有节点 STANDBY。单节点绕过 记录在会话
apikeys 路由 whitelist 已修:WhitelistPOSTURL 加 /xce/v1/apikeys bug-1489(已修)
mcp_operator 缺 queue:control 已修:rbac.yaml 加 queue:control(job-control handler 也检查它) —
HA 3 节点需 xceadmin push xceadmin config push 有 discovery.py 解包 bug(deployment.md 已知) 已知

8. 快速参考

操作 命令
查 controller 进程 sudo fuser 9990/tcp
查 apiserver curl -s http://localhost:8080/xce/v1/cluster
controller 日志 sudo tail /opt/xce/logs/xce_controller.log
apiserver 日志 sudo tail /opt/xce/apiserver/logs/apiserver_out.log
停 controller sudo fuser -k 9990/tcp
重启 apiserver kill $(fuser 8080/tcp); cd /opt/newxce && setsid ./run.sh apiserver -d /opt/xce/apiserver &
本地 Go 测试 见 §6