@baize-ai/core 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +2 -0
- package/CHANGELOG.md +17 -0
- package/Dockerfile +38 -14
- package/docker/entrypoint.sh +24 -8
- package/package.json +1 -1
- package/scripts/build-push-acr.sh +38 -0
- package/scripts/docker-publish.sh +12 -1
- package/scripts/install-docker.sh +35 -0
- package/scripts/install.sh +13 -0
- package/skills/web-console/public/app.js +179 -0
- package/skills/web-console/public/index.html +18 -4
- package/skills/web-console/scripts/a2a-admin.js +41 -0
- package/skills/web-console/scripts/diagnostics.js +45 -1
- package/skills/web-console/scripts/server.js +290 -2
package/.dockerignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to baize-core will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.13] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- A2A 上传安装后自动 rebuild 原生模块(D44):上传安装用 --ignore-scripts(vendor 包安全),better-sqlite3 等 native 模块不编译 → 上传后 A2A 状态加载失败;安装流程现追加 `npm rebuild --build-from-source`(只编译、不跑 vendor 脚本),失败回滚
|
|
12
|
+
- Docker 镜像 npm 源修复(D43):容器 npm 运行时是 baize 用户(prefix /home/baize/.npm-global)——镜像只写了 /usr/etc/npmrc(root 视角)npm 不读 → ACR 镜像 npm 仍走 npmjs;现同时写 /home/baize/.npmrc(优先级最高必读)
|
|
13
|
+
|
|
14
|
+
## [0.3.12] - 2026-08-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- web 控制台 runtime 未安装时异步安装(D39):切换 claude/codex 检测 CLI 缺失 → 后台安装(install.sh/npm,900s 超时 + 重试)→ 装完自动切换;状态端点 + 前端进度轮询(修复 240s/300s 双超时导致的切 cc 失败)
|
|
18
|
+
- 初始化失败 web 可见(D40):entrypoint 写 init-state.json,诊断卡显示「初始化未完成」+ 引导
|
|
19
|
+
- runtime 安装状态可见(D41):模型设置页每个 runtime 显示安装徽章(✅ 版本 / ❌ 未装 + 失败原因)+ [安装] 按钮(复用 D39 异步安装)+ 当前 runtime 缺失红色警告
|
|
20
|
+
- 环境修复闭环(D42):环境初始化卡置顶(未就绪缺口人话 + [重新初始化] 一键幂等 init + 失败人话原因 + web 内日志展开)——web 是唯一恢复入口,无需 SSH
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Docker 镜像:不捆绑 claude(单架构二进制导致另一平台 exec format error);npm/apt 源跟随推送目标(ghcr 默认官方源,ACR 构建全 npmmirror/阿里云);entrypoint 首次默认 codex(与 install.sh 统一,已初始化尊重现有 runtime)
|
|
24
|
+
|
|
8
25
|
## [0.3.11] - 2026-08-27
|
|
9
26
|
|
|
10
27
|
### Fixed
|
package/Dockerfile
CHANGED
|
@@ -14,8 +14,21 @@ FROM node:22-slim
|
|
|
14
14
|
|
|
15
15
|
LABEL org.opencontainers.image.source="https://github.com/baize01-ai/baize-core"
|
|
16
16
|
LABEL org.opencontainers.image.description="Baize — autonomous AI agent infrastructure"
|
|
17
|
+
# Build flags: official sources by default (npmjs + Debian). Set to "aliyun"
|
|
18
|
+
# when building for the Aliyun ACR (mainland distribution) — npmmirror npm +
|
|
19
|
+
# Aliyun Debian mirror, baked into the image so docker run npm also uses the
|
|
20
|
+
# mainland source. ARG must precede the RUNs that reference them.
|
|
21
|
+
ARG BAIZE_APT_MIRROR=off
|
|
22
|
+
ARG BAIZE_NPM_MIRROR=off
|
|
17
23
|
|
|
18
24
|
# ── System packages ───────────────────────────────────────────────────────────
|
|
25
|
+
# Point apt at the Aliyun Debian mirror when building on mainland servers
|
|
26
|
+
# (deb.debian.org is unreachable/slow there; env flag lets non-CN builds keep
|
|
27
|
+
# the official source).
|
|
28
|
+
RUN if [ "${BAIZE_APT_MIRROR:-off}" = "aliyun" ]; then \
|
|
29
|
+
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources; \
|
|
30
|
+
sed -i 's|security.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources; \
|
|
31
|
+
fi
|
|
19
32
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
20
33
|
git \
|
|
21
34
|
curl \
|
|
@@ -35,22 +48,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
35
48
|
g++ \
|
|
36
49
|
&& rm -rf /var/lib/apt/lists/*
|
|
37
50
|
|
|
38
|
-
#
|
|
51
|
+
# Mainland: registry.npmjs.org is unreachable — point npm at npmmirror
|
|
52
|
+
# (build-arg BAIZE_NPM_MIRROR=off keeps the official registry).
|
|
53
|
+
RUN if [ "${BAIZE_NPM_MIRROR:-off}" = "aliyun" ]; then \
|
|
54
|
+
npm config set registry https://registry.npmmirror.com --global; \
|
|
55
|
+
# Also drop a system-wide npmrc so the runtime user (baize) and any
|
|
56
|
+
# docker exec npm use the mainland mirror too.
|
|
57
|
+
mkdir -p /usr/etc && printf 'registry=https://registry.npmmirror.com\n' > /usr/etc/npmrc; \
|
|
58
|
+
# Container npm runs as USER baize with prefix /home/baize/.npm-global —
|
|
59
|
+
# its globalconfig is NOT /usr/etc/npmrc, so also write the user-level
|
|
60
|
+
# ~/.npmrc (highest precedence, always read) or npm stays on npmjs.
|
|
61
|
+
mkdir -p /home/baize && printf 'registry=https://registry.npmmirror.com\n' > /home/baize/.npmrc; \
|
|
62
|
+
fi
|
|
39
63
|
RUN npm install -g pm2@latest
|
|
40
64
|
|
|
41
|
-
# ── Claude Code CLI (pre-bundled, pinned version) ─────────────────────────────
|
|
42
|
-
# The official install.sh downloads a ~340MB binary at container runtime, which
|
|
43
|
-
# is unreliable inside container networks (fails ~90% through on Docker
|
|
44
|
-
# Desktop). Bundle a PINNED, verified binary in the image instead:
|
|
45
|
-
# curl -fsSL https://downloads.claude.ai/claude-code-releases/<ver>/linux-arm64/claude
|
|
46
|
-
# (verify against .../manifest.json platforms.linux-arm64 checksum)
|
|
47
|
-
# echo <ver> > .claude-local/claude.version
|
|
48
|
-
# Upgrade = replace the binary + bump claude.version, then rebuild the image
|
|
49
|
-
# (explicit release decision — never auto-fetch latest at build time).
|
|
50
|
-
COPY .claude-local/claude /usr/local/bin/claude
|
|
51
|
-
RUN chmod +x /usr/local/bin/claude
|
|
52
|
-
COPY .claude-local/claude.version /usr/local/bin/claude.version
|
|
53
|
-
LABEL org.opencontainers.image.claude-version="$(cat .claude-local/claude.version 2>/dev/null || echo unknown)"
|
|
54
65
|
|
|
55
66
|
# ── Create baize user (non-root) ──────────────────────────────────────────────
|
|
56
67
|
RUN useradd -m -s /bin/bash baize \
|
|
@@ -71,6 +82,11 @@ ENV WEB_CONSOLE_BIND=0.0.0.0
|
|
|
71
82
|
# build context) — source stays private, the image matches the npm version
|
|
72
83
|
# exactly. Build with: docker build --build-arg BAZE_CORE_VERSION=0.2.0 .
|
|
73
84
|
ARG BAZE_CORE_VERSION=latest
|
|
85
|
+
# Local-development override: when building from source with
|
|
86
|
+
# --build-arg BAIZE_LOCAL_SKILLS=1, copy the LOCAL skills over the npm-package
|
|
87
|
+
# skills so un-released fixes (e.g. D44) land in the image without publishing.
|
|
88
|
+
# Official builds leave this off — the published npm package is authoritative.
|
|
89
|
+
ARG BAIZE_LOCAL_SKILLS=0
|
|
74
90
|
WORKDIR /home/baize
|
|
75
91
|
RUN npm install -g @baize-ai/core@${BAZE_CORE_VERSION} \
|
|
76
92
|
&& baize --version \
|
|
@@ -113,6 +129,14 @@ RUN npm install -g @baize-ai/core@${BAZE_CORE_VERSION} \
|
|
|
113
129
|
&& node -e "const D=require('/home/baize/baize/.claude/skills/web-console/node_modules/better-sqlite3'); new D(':memory:')" \
|
|
114
130
|
&& echo "native modules OK (locally compiled, baked into runtime skills)"
|
|
115
131
|
|
|
132
|
+
# ── Local-source skills override (BAIZE_LOCAL_SKILLS=1) ─────────────────────
|
|
133
|
+
# docker-publish.sh stages the repo's skills/ into .docker-local-skills/ when
|
|
134
|
+
# BAIZE_LOCAL_SKILLS=1 (empty directory otherwise). They land in
|
|
135
|
+
# /home/baize/.local-skills and are applied by entrypoint AFTER init, because
|
|
136
|
+
# init's postinstall syncSkills uses the npm-package skills as source and
|
|
137
|
+
# would otherwise overwrite them. Official images carry an empty dir — no-op.
|
|
138
|
+
COPY --chown=baize:baize .docker-local-skills/ /home/baize/.local-skills/
|
|
139
|
+
|
|
116
140
|
# ── Workspace directories ─────────────────────────────────────────────────────
|
|
117
141
|
# ~/baize is mounted as a single volume in docker-compose.yml.
|
|
118
142
|
# Creating subdirectories here ensures correct ownership in the image.
|
package/docker/entrypoint.sh
CHANGED
|
@@ -65,15 +65,14 @@ if [ -n "${AUTH_TOKEN}" ]; then
|
|
|
65
65
|
fi
|
|
66
66
|
fi
|
|
67
67
|
|
|
68
|
-
# Detect runtime —
|
|
69
|
-
#
|
|
68
|
+
# Detect runtime — default to codex ONLY on first boot (mainland default;
|
|
69
|
+
# the image no longer bundles claude, which is installed manually when
|
|
70
|
+
# needed). If the workspace is already initialized (config.json exists) the
|
|
71
|
+
# existing runtime is respected — init would otherwise reset a runtime the
|
|
72
|
+
# user switched to in the web console. BAIZE_RUNTIME env always wins.
|
|
70
73
|
RUNTIME_FLAG=""
|
|
71
74
|
if [ -z "${BAIZE_RUNTIME:-}" ]; then
|
|
72
|
-
|
|
73
|
-
HAS_CODEX_AUTH=false
|
|
74
|
-
[ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && HAS_CLAUDE_AUTH=true
|
|
75
|
-
[ -n "${OPENAI_API_KEY:-}" ] || [ -n "${CODEX_API_KEY:-}" ] && HAS_CODEX_AUTH=true
|
|
76
|
-
if [ "${HAS_CODEX_AUTH}" = true ] && [ "${HAS_CLAUDE_AUTH}" = false ]; then
|
|
75
|
+
if [ ! -f "${BAIZE_DIR}/.baize/config.json" ]; then
|
|
77
76
|
RUNTIME_FLAG="--runtime codex"
|
|
78
77
|
fi
|
|
79
78
|
fi
|
|
@@ -87,9 +86,26 @@ INIT_ARGS="--yes --quiet"
|
|
|
87
86
|
# shellcheck disable=SC2086
|
|
88
87
|
if ! baize init ${INIT_ARGS}; then
|
|
89
88
|
warn "baize init exited with errors (may be partial). Check logs."
|
|
89
|
+
# Surface init failure to the web console (diagnostics card) so the user can
|
|
90
|
+
# discover and fix it in-browser (e.g. a runtime CLI that failed to install).
|
|
91
|
+
mkdir -p "${BAIZE_DIR}/.baize"
|
|
92
|
+
printf '{"status":"failed","error":"baize init exited with errors","at":"%s"}\n' "$(date -Iseconds)" > "${BAIZE_DIR}/.baize/init-state.json"
|
|
93
|
+
exit 1
|
|
90
94
|
fi
|
|
91
|
-
|
|
92
95
|
ok "Workspace ready"
|
|
96
|
+
mkdir -p "${BAIZE_DIR}/.baize"
|
|
97
|
+
# ── Local-source skills override (BAIZE_LOCAL_SKILLS=1 images) ──────────────
|
|
98
|
+
# Images built with BAIZE_LOCAL_SKILLS=1 bake the repo's skills into
|
|
99
|
+
# /home/baize/.local-skills. syncSkills (npm postinstall) runs during init
|
|
100
|
+
# with the npm-package skills as source, so apply the local override AFTER
|
|
101
|
+
# init to keep un-released fixes (e.g. D44) authoritative.
|
|
102
|
+
# Official images ship an empty .local-skills — this is a no-op there.
|
|
103
|
+
if [ -d /home/baize/.local-skills ]; then
|
|
104
|
+
cp -rf /home/baize/.local-skills/. "${BAIZE_DIR}/.claude/skills/" 2>/dev/null \
|
|
105
|
+
&& ok "Local-source skills applied (BAIZE_LOCAL_SKILLS=1)"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# ── Pass through channel env vars to .env ─────────────────────────────────────
|
|
93
109
|
|
|
94
110
|
# ── Pass through channel env vars to .env ─────────────────────────────────────
|
|
95
111
|
# baize init doesn't write channel tokens — those come from component installs.
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Multi-platform build + push to Aliyun ACR for the mainland distribution.
|
|
3
|
+
# Usage: bash build-push-acr.sh [VERSION] (default 0.3.11)
|
|
4
|
+
# Requires: docker login already done, qemu-arm64 registered
|
|
5
|
+
# (apt install qemu-user-binfmt), registry mirror configured.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
VERSION="${1:-0.3.11}"
|
|
9
|
+
ACR="crpi-8bg62qbrjs3cr3as.cn-hangzhou.personal.cr.aliyuncs.com/baize01/baize-core"
|
|
10
|
+
|
|
11
|
+
echo "== Configure buildkit registry mirror (mainland Docker Hub access) =="
|
|
12
|
+
mkdir -p /etc/buildkit
|
|
13
|
+
cat > /etc/buildkit/buildkit.toml <<'EOF'
|
|
14
|
+
[registry."docker.io"]
|
|
15
|
+
mirrors = ["https://docker.m.daocloud.io"]
|
|
16
|
+
EOF
|
|
17
|
+
|
|
18
|
+
echo "== Ensure multi-platform builder (reuse for layer cache) =="
|
|
19
|
+
if ! docker buildx inspect multi >/dev/null 2>&1; then
|
|
20
|
+
docker buildx create --name multi --driver docker-container \
|
|
21
|
+
--config /etc/buildkit/buildkit.toml --use
|
|
22
|
+
fi
|
|
23
|
+
docker buildx use multi 2>/dev/null || true
|
|
24
|
+
|
|
25
|
+
echo "== Build + push $VERSION (amd64 + arm64) =="
|
|
26
|
+
docker buildx build \
|
|
27
|
+
--platform linux/amd64,linux/arm64 \
|
|
28
|
+
--provenance=false --sbom=false \
|
|
29
|
+
--build-arg BAZE_CORE_VERSION="$VERSION" \
|
|
30
|
+
--build-arg BAIZE_APT_MIRROR=aliyun \
|
|
31
|
+
--build-arg BAIZE_NPM_MIRROR=aliyun \
|
|
32
|
+
-t "$ACR:$VERSION" \
|
|
33
|
+
-t "$ACR:latest" \
|
|
34
|
+
--push .
|
|
35
|
+
|
|
36
|
+
echo "== Verify =="
|
|
37
|
+
docker buildx imagetools inspect "$ACR:$VERSION" | grep -E "Platform|Architecture" | head -4
|
|
38
|
+
echo "== Done =="
|
|
@@ -30,11 +30,22 @@ ACR_NAMESPACE="${ACR_NAMESPACE:-baize01}"
|
|
|
30
30
|
|
|
31
31
|
# ── Build ────────────────────────────────────────────────────────────────────
|
|
32
32
|
echo "==> Building baize-core:${VERSION} (from npm @baize-ai/core@${VERSION})"
|
|
33
|
+
BUILD_ARGS=(--build-arg "BAZE_CORE_VERSION=${VERSION}")
|
|
34
|
+
rm -rf .docker-local-skills && mkdir -p .docker-local-skills
|
|
35
|
+
if [ "${BAIZE_LOCAL_SKILLS:-0}" = "1" ]; then
|
|
36
|
+
BUILD_ARGS+=(--build-arg BAIZE_LOCAL_SKILLS=1)
|
|
37
|
+
echo "==> Overriding skills with LOCAL repo source (BAIZE_LOCAL_SKILLS=1) — un-released fixes baked in"
|
|
38
|
+
cp -r skills/. .docker-local-skills/
|
|
39
|
+
# Never ship host-compiled node_modules (mac binaries break linux runtime) —
|
|
40
|
+
# dependency binaries come from the image's own npm install.
|
|
41
|
+
find .docker-local-skills -type d -name node_modules -prune -exec rm -rf {} +
|
|
42
|
+
fi
|
|
33
43
|
docker build \
|
|
34
|
-
|
|
44
|
+
"${BUILD_ARGS[@]}" \
|
|
35
45
|
-t "${GHCR_IMAGE}:${VERSION}" \
|
|
36
46
|
-t "${GHCR_IMAGE}:latest" \
|
|
37
47
|
.
|
|
48
|
+
rm -rf .docker-local-skills
|
|
38
49
|
|
|
39
50
|
if [ -n "${ACR_REGISTRY}" ] && [ -n "${ACR_NAMESPACE}" ]; then
|
|
40
51
|
ACR_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/baize-core"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install Docker Engine (with buildx) on a mainland Ubuntu server.
|
|
3
|
+
# Uses the Aliyun docker-ce mirror; falls back from the local Ubuntu
|
|
4
|
+
# codename to noble (24.04) when the mirror has not synced it yet.
|
|
5
|
+
# Run as root: bash install-docker.sh
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
echo "== 1/4 Download Docker GPG key =="
|
|
9
|
+
curl -fsSL -o /tmp/docker.gpg https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg
|
|
10
|
+
gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg /tmp/docker.gpg
|
|
11
|
+
|
|
12
|
+
echo "== 2/4 Write apt source (deb822) =="
|
|
13
|
+
CODENAME=$(lsb_release -cs)
|
|
14
|
+
{
|
|
15
|
+
echo "Types: deb"
|
|
16
|
+
echo "URIs: https://mirrors.aliyun.com/docker-ce/linux/ubuntu"
|
|
17
|
+
echo "Suites: $CODENAME"
|
|
18
|
+
echo "Components: stable"
|
|
19
|
+
echo "Signed-By: /usr/share/keyrings/docker-archive-keyring.gpg"
|
|
20
|
+
} > /etc/apt/sources.list.d/docker.sources
|
|
21
|
+
|
|
22
|
+
if ! apt-get update; then
|
|
23
|
+
echo "== $CODENAME not on the mirror yet, falling back to noble =="
|
|
24
|
+
sed -i "s/Suites: $CODENAME/Suites: noble/" /etc/apt/sources.list.d/docker.sources
|
|
25
|
+
apt-get update
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
echo "== 3/4 Install Docker =="
|
|
29
|
+
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
|
30
|
+
|
|
31
|
+
echo "== 4/4 Start and verify =="
|
|
32
|
+
systemctl enable --now docker
|
|
33
|
+
docker --version
|
|
34
|
+
docker buildx version
|
|
35
|
+
echo "== Done =="
|
package/scripts/install.sh
CHANGED
|
@@ -314,6 +314,7 @@ _ensure_path_in_profile() {
|
|
|
314
314
|
# Added by baize installer
|
|
315
315
|
fish_add_path -g $HOME/.local/bin
|
|
316
316
|
fish_add_path -g $HOME/baize/bin
|
|
317
|
+
fish_add_path -g $HOME/.local/node/bin
|
|
317
318
|
FISH_EOF
|
|
318
319
|
ok "PATH configured in conf.d/baize.fish"
|
|
319
320
|
fi
|
|
@@ -331,15 +332,26 @@ FISH_EOF
|
|
|
331
332
|
local baize_marker='# baize-managed: bin PATH'
|
|
332
333
|
local baize_bin_export="export PATH=\"\$HOME/baize/bin:\$PATH\""
|
|
333
334
|
|
|
335
|
+
# 3. ~/.local/node/bin — Node/npm installed from the npmmirror binary
|
|
336
|
+
# mirror (or a bundled Node). Idempotency: dedicated marker.
|
|
337
|
+
local node_marker='# baize-managed: node bin PATH'
|
|
338
|
+
local node_bin_export="export PATH=\"\$HOME/.local/node/bin:\$PATH\""
|
|
339
|
+
|
|
334
340
|
# Write to ~/.profile (login shells + non-interactive shells)
|
|
335
341
|
if ! grep -q 'baize-managed: bin PATH' "$HOME/.profile" 2>/dev/null; then
|
|
336
342
|
printf '\n%s\n%s\n' "$baize_marker" "$baize_bin_export" >> "$HOME/.profile"
|
|
337
343
|
fi
|
|
344
|
+
if ! grep -q 'baize-managed: node bin PATH' "$HOME/.profile" 2>/dev/null; then
|
|
345
|
+
printf '\n%s\n%s\n' "$node_marker" "$node_bin_export" >> "$HOME/.profile"
|
|
346
|
+
fi
|
|
338
347
|
# Write to shell rc file (interactive shells)
|
|
339
348
|
if [ "$shell_rc" != "$HOME/.profile" ]; then
|
|
340
349
|
if ! grep -q 'baize-managed: bin PATH' "$shell_rc" 2>/dev/null; then
|
|
341
350
|
printf '\n%s\n%s\n' "$baize_marker" "$baize_bin_export" >> "$shell_rc"
|
|
342
351
|
fi
|
|
352
|
+
if ! grep -q 'baize-managed: node bin PATH' "$shell_rc" 2>/dev/null; then
|
|
353
|
+
printf '\n%s\n%s\n' "$node_marker" "$node_bin_export" >> "$shell_rc"
|
|
354
|
+
fi
|
|
343
355
|
fi
|
|
344
356
|
|
|
345
357
|
ok "PATH configured in $(basename "$shell_rc")"
|
|
@@ -347,6 +359,7 @@ FISH_EOF
|
|
|
347
359
|
|
|
348
360
|
# Export for the running script (so baize init can find binaries)
|
|
349
361
|
export PATH="$HOME/.local/bin:$HOME/baize/bin:$PATH"
|
|
362
|
+
export PATH="$HOME/.local/node/bin:$PATH"
|
|
350
363
|
}
|
|
351
364
|
|
|
352
365
|
# ── Install Baize ─────────────────────────────────────────────
|
|
@@ -972,6 +972,13 @@ async function switchRuntimeFromAdmin(basePath) {
|
|
|
972
972
|
setAdminMsg('未登录或会话过期,请重新登录', true);
|
|
973
973
|
return;
|
|
974
974
|
}
|
|
975
|
+
if (body.installing) {
|
|
976
|
+
// D39: runtime binary missing — async install in progress, poll status.
|
|
977
|
+
const label = target === 'claude' ? 'Claude Code' : 'Codex';
|
|
978
|
+
setAdminMsg(`正在安装 ${label}(下载约 340MB,首次约 2-5 分钟)...`);
|
|
979
|
+
pollRuntimeInstall(basePath, target);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
975
982
|
if (body.success) {
|
|
976
983
|
const restartNote = body.restart?.restarted ? ',会话已自动重启' : (body.restart?.reason === 'no_session' ? '(无运行中会话)' : '');
|
|
977
984
|
setAdminMsg(`已切换到 ${target}${restartNote}`);
|
|
@@ -981,6 +988,165 @@ async function switchRuntimeFromAdmin(basePath) {
|
|
|
981
988
|
loadAdminStatus(basePath);
|
|
982
989
|
}
|
|
983
990
|
|
|
991
|
+
async function pollRuntimeInstall(basePath, runtime) {
|
|
992
|
+
const label = runtime === 'claude' ? 'Claude Code' : 'Codex';
|
|
993
|
+
let attempts = 0;
|
|
994
|
+
while (attempts < 400) { // ~20 min cap
|
|
995
|
+
attempts += 1;
|
|
996
|
+
const { status, body } = await adminFetch(basePath, `/api/admin/runtime/install-status?runtime=${encodeURIComponent(runtime)}`);
|
|
997
|
+
if (status !== 200 || !body) {
|
|
998
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (body.status === 'done') {
|
|
1002
|
+
setAdminMsg(`${label} 安装完成,已切换到 ${runtime},会话重启中`);
|
|
1003
|
+
renderRuntimeInstallBadges(basePath);
|
|
1004
|
+
loadAdminStatus(basePath);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (body.status === 'failed') {
|
|
1008
|
+
setAdminMsg(`${label} 安装失败:${body.error || '未知错误'}(可稍后重试)`, true);
|
|
1009
|
+
renderRuntimeInstallBadges(basePath);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
1013
|
+
}
|
|
1014
|
+
setAdminMsg(`${label} 安装仍在进行,请稍后查看状态`, true);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// D41: per-runtime install badges (installed/version/failure) + install button
|
|
1018
|
+
// (reuses D39 async install) + a warning when the ACTIVE runtime is missing.
|
|
1019
|
+
async function renderRuntimeInstallBadges(basePath) {
|
|
1020
|
+
const badgeClaude = document.getElementById('rt-install-claude');
|
|
1021
|
+
const badgeCodex = document.getElementById('rt-install-codex');
|
|
1022
|
+
const warnEl = document.getElementById('rt-install-warning');
|
|
1023
|
+
if (!badgeClaude || !badgeCodex) return;
|
|
1024
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/diagnostics');
|
|
1025
|
+
if (status !== 200 || !body?.runtimes) return;
|
|
1026
|
+
const rt = body.runtime?.runtime || 'claude';
|
|
1027
|
+
const rtInfo = body.runtimes || {};
|
|
1028
|
+
const badge = (name, info) => {
|
|
1029
|
+
const el = name === 'claude' ? badgeClaude : badgeCodex;
|
|
1030
|
+
if (info.installed) {
|
|
1031
|
+
el.innerHTML = `<span class="channel-state ok">✅ 已安装${info.version ? ` ${escapeHtml(info.version)}` : ''}</span>`;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const reason = info.installError ? `(上次失败:${escapeHtml(info.installError)})` : '';
|
|
1035
|
+
el.innerHTML = `<span class="channel-state warn">❌ 未安装${reason}</span> `
|
|
1036
|
+
+ `<button type="button" class="small-btn" data-install="${name}">安装</button>`;
|
|
1037
|
+
};
|
|
1038
|
+
badge('claude', rtInfo.claude || {});
|
|
1039
|
+
badge('codex', rtInfo.codex || {});
|
|
1040
|
+
// Warn when the active runtime CLI is missing — the session cannot start.
|
|
1041
|
+
if (warnEl) {
|
|
1042
|
+
const active = rtInfo[rt] || {};
|
|
1043
|
+
if (!active.installed) {
|
|
1044
|
+
const label = rt === 'claude' ? 'Claude Code' : 'Codex';
|
|
1045
|
+
warnEl.innerHTML = `<span class="channel-state warn">⚠ 会话无法启动:当前运行时 ${escapeHtml(rt)} 未安装——请点上方[安装]或[切换到 ${rt === 'claude' ? 'codex' : 'claude'}]</span>`;
|
|
1046
|
+
} else {
|
|
1047
|
+
warnEl.textContent = '';
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// Wire install buttons (delegate once per render is fine — small page).
|
|
1051
|
+
document.querySelectorAll('[data-install]').forEach((btn) => {
|
|
1052
|
+
btn.onclick = async () => {
|
|
1053
|
+
const r = btn.getAttribute('data-install');
|
|
1054
|
+
btn.disabled = true;
|
|
1055
|
+
btn.textContent = '安装中...';
|
|
1056
|
+
setAdminMsg(`正在安装 ${r === 'claude' ? 'Claude Code' : 'Codex'}(下载约 340MB,首次约 2-5 分钟)...`);
|
|
1057
|
+
const rr = await adminFetch(basePath, '/api/admin/runtime', {
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1060
|
+
body: JSON.stringify({ runtime: r }),
|
|
1061
|
+
});
|
|
1062
|
+
if (rr.body?.installing) {
|
|
1063
|
+
pollRuntimeInstall(basePath, r);
|
|
1064
|
+
} else if (rr.body?.success) {
|
|
1065
|
+
setAdminMsg(`${r === 'claude' ? 'Claude Code' : 'Codex'} 已安装`);
|
|
1066
|
+
renderRuntimeInstallBadges(basePath);
|
|
1067
|
+
} else {
|
|
1068
|
+
setAdminMsg(`安装触发失败:${rr.body?.error || '未知错误'}`, true);
|
|
1069
|
+
btn.disabled = false;
|
|
1070
|
+
btn.textContent = '安装';
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// D42: environment status card — gaps in plain language, one-click re-init,
|
|
1077
|
+
// failure reason (human) + in-web log.
|
|
1078
|
+
async function renderEnvironmentStatus(basePath) {
|
|
1079
|
+
const badge = document.getElementById('env-state-badge');
|
|
1080
|
+
const detail = document.getElementById('env-state-detail');
|
|
1081
|
+
const actions = document.getElementById('env-reinit-actions');
|
|
1082
|
+
if (!badge || !detail) return;
|
|
1083
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/environment/status');
|
|
1084
|
+
if (status !== 200 || !body) { detail.textContent = '环境状态检测失败'; return; }
|
|
1085
|
+
const rt = body.runtime || 'codex';
|
|
1086
|
+
const claudeOk = body.runtimes?.claude?.installed ? '✅' : '❌';
|
|
1087
|
+
const codexOk = body.runtimes?.codex?.installed ? '✅' : '❌';
|
|
1088
|
+
const initOk = body.initComplete ? '✅' : '❌';
|
|
1089
|
+
if (body.ready) {
|
|
1090
|
+
badge.textContent = '✅ 就绪';
|
|
1091
|
+
badge.className = 'channel-state ok';
|
|
1092
|
+
detail.innerHTML = `运行时 ${escapeHtml(rt)} · CLI ${rt === 'claude' ? claudeOk : codexOk} · 初始化 ${initOk}`;
|
|
1093
|
+
actions.style.display = 'none';
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
badge.textContent = '⚠ 未就绪';
|
|
1097
|
+
badge.className = 'channel-state warn';
|
|
1098
|
+
const gapLines = (body.gaps || []).map((g) => `<li>${escapeHtml(g)}</li>`).join('');
|
|
1099
|
+
detail.innerHTML = `<ul style="margin:4px 0;padding-left:18px">${gapLines || '<li>环境未就绪</li>'}</ul>`
|
|
1100
|
+
+ `<span style="font-size:12px;opacity:.8">Codex ${codexOk} · Claude ${claudeOk} · 初始化 ${initOk}</span>`;
|
|
1101
|
+
actions.style.display = 'block';
|
|
1102
|
+
renderReinitStatus(basePath);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function renderReinitStatus(basePath) {
|
|
1106
|
+
const prog = document.getElementById('env-reinit-progress');
|
|
1107
|
+
const logBox = document.getElementById('env-reinit-log');
|
|
1108
|
+
const logBody = document.getElementById('env-reinit-log-body');
|
|
1109
|
+
if (!prog) return;
|
|
1110
|
+
const { status, body } = await adminFetch(basePath, '/api/admin/environment/reinit-status');
|
|
1111
|
+
if (status !== 200 || !body || body.status === 'idle') { prog.textContent = ''; return; }
|
|
1112
|
+
if (body.status === 'running') {
|
|
1113
|
+
prog.textContent = '正在重新初始化(补全环境配置,约 1-2 分钟)...';
|
|
1114
|
+
logBox.style.display = 'none';
|
|
1115
|
+
} else if (body.status === 'done') {
|
|
1116
|
+
prog.innerHTML = '<span class="channel-state ok">✅ 初始化完成,环境已就绪,会话拉起中...</span>';
|
|
1117
|
+
logBox.style.display = 'none';
|
|
1118
|
+
setTimeout(() => renderEnvironmentStatus(basePath), 5000);
|
|
1119
|
+
loadAdminStatus(basePath);
|
|
1120
|
+
} else if (body.status === 'failed') {
|
|
1121
|
+
prog.innerHTML = `<span class="channel-state warn">⚠ 初始化失败:${escapeHtml(body.human || '未知原因')}</span>`;
|
|
1122
|
+
if (body.logTail) {
|
|
1123
|
+
logBody.textContent = body.logTail;
|
|
1124
|
+
logBox.style.display = 'block';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function startReinitFromAdmin(basePath) {
|
|
1130
|
+
const btn = document.getElementById('btn-env-reinit');
|
|
1131
|
+
if (!btn) return;
|
|
1132
|
+
btn.disabled = true;
|
|
1133
|
+
btn.textContent = '重新初始化中...';
|
|
1134
|
+
setAdminMsg('正在重新初始化环境(幂等,不会清除已有数据)...');
|
|
1135
|
+
adminFetch(basePath, '/api/admin/environment/reinit', { method: 'POST' }).then(({ body }) => {
|
|
1136
|
+
if (!body?.reinit) { setAdminMsg('重新初始化未能启动', true); btn.disabled = false; btn.textContent = '重新初始化(修复环境)'; return; }
|
|
1137
|
+
const iv = setInterval(async () => {
|
|
1138
|
+
const { status, body: b } = await adminFetch(basePath, '/api/admin/environment/reinit-status');
|
|
1139
|
+
if (status !== 200 || !b) return;
|
|
1140
|
+
renderReinitStatus(basePath);
|
|
1141
|
+
if (b.status === 'done' || b.status === 'failed') {
|
|
1142
|
+
clearInterval(iv);
|
|
1143
|
+
btn.disabled = false;
|
|
1144
|
+
btn.textContent = '重新初始化(修复环境)';
|
|
1145
|
+
}
|
|
1146
|
+
}, 3000);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
984
1150
|
async function restartSessionFromAdmin(basePath) {
|
|
985
1151
|
setAdminMsg('正在重启 agent 会话...');
|
|
986
1152
|
const { body } = await adminFetch(basePath, '/api/admin/restart', { method: 'POST' });
|
|
@@ -2319,6 +2485,16 @@ async function renderDiagnostics(basePath) {
|
|
|
2319
2485
|
}
|
|
2320
2486
|
const rt = body.runtime || {};
|
|
2321
2487
|
if (stateEl) stateEl.textContent = `runtime: ${escapeHtml(rt.runtime || '?')}`;
|
|
2488
|
+
// init state (D40): surface init failure so the user can act in-browser
|
|
2489
|
+
const init = body.init || null;
|
|
2490
|
+
if (init && init.status === 'failed') {
|
|
2491
|
+
const initEl = document.getElementById('diag-init-state');
|
|
2492
|
+
if (initEl) {
|
|
2493
|
+
initEl.innerHTML = '<span class="channel-state warn">⚠ 初始化未完成:'
|
|
2494
|
+
+ `${escapeHtml(init.error || '未知原因')}</span> —— 请在「模型设置」安装/重试对应 runtime(Claude Code 或 Codex),然后重启 agent 会话。`
|
|
2495
|
+
+ (init.at ? ` <small>(${escapeHtml(String(init.at))})</small>` : '');
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2322
2498
|
// runtime + auth
|
|
2323
2499
|
const auth = body.auth || {};
|
|
2324
2500
|
const authLine = [];
|
|
@@ -2382,6 +2558,8 @@ function showAppView(basePath, view) {
|
|
|
2382
2558
|
schedulerView.hidden = true;
|
|
2383
2559
|
modelView.hidden = false;
|
|
2384
2560
|
renderDiagnostics(basePath);
|
|
2561
|
+
renderRuntimeInstallBadges(basePath);
|
|
2562
|
+
renderEnvironmentStatus(basePath);
|
|
2385
2563
|
setNav(navModel, [navChat, navChannels, navA2a, navScheduler]);
|
|
2386
2564
|
renderModelSettings(basePath);
|
|
2387
2565
|
} else if (view === 'channels') {
|
|
@@ -2587,6 +2765,7 @@ function initViews(basePath) {
|
|
|
2587
2765
|
});
|
|
2588
2766
|
document.getElementById('btn-switch-runtime').addEventListener('click', () => switchRuntimeFromAdmin(basePath));
|
|
2589
2767
|
document.getElementById('btn-restart-session').addEventListener('click', () => restartSessionFromAdmin(basePath));
|
|
2768
|
+
document.getElementById('btn-env-reinit')?.addEventListener('click', () => startReinitFromAdmin(basePath));
|
|
2590
2769
|
|
|
2591
2770
|
document.getElementById('form-provider').addEventListener('submit', (e) => {
|
|
2592
2771
|
e.preventDefault();
|
|
@@ -109,14 +109,27 @@
|
|
|
109
109
|
<!-- Section 0: Current connection summary -->
|
|
110
110
|
<section class="settings-section">
|
|
111
111
|
<div class="settings-section-head">
|
|
112
|
-
<h2
|
|
113
|
-
<p
|
|
112
|
+
<h2>运行时与连接</h2>
|
|
113
|
+
<p>环境未就绪时,请先完成「环境初始化」(可自动安装缺失组件并补全配置),再配置运行时接入方式。</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="settings-card">
|
|
116
|
+
<div class="card-title">环境初始化 <span class="channel-state" id="env-state-badge"></span></div>
|
|
117
|
+
<div class="cred-state" id="env-state-detail" aria-live="polite">检测中...</div>
|
|
118
|
+
<div class="settings-actions" id="env-reinit-actions" style="display:none">
|
|
119
|
+
<button type="button" class="btn btn-primary" id="btn-env-reinit">重新初始化(一键修复环境)</button>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="cred-state" id="env-reinit-progress" aria-live="polite"></div>
|
|
122
|
+
<details id="env-reinit-log" style="display:none;margin-top:8px">
|
|
123
|
+
<summary>查看初始化日志</summary>
|
|
124
|
+
<pre style="font-size:12px;background:var(--bg-2,#1a1a1a);padding:8px;border-radius:6px;white-space:pre-wrap;max-height:300px;overflow:auto" id="env-reinit-log-body"></pre>
|
|
125
|
+
</details>
|
|
114
126
|
</div>
|
|
115
127
|
<div class="settings-card">
|
|
116
128
|
<div class="conn-summary" id="conn-summary" role="status" aria-live="polite">
|
|
117
|
-
<div class="conn-summary-row"><span class="conn-summary-name">Codex</span><span class="conn-summary-detail" id="summary-codex">加载中...</span></div>
|
|
118
|
-
<div class="conn-summary-row"><span class="conn-summary-name">Claude</span><span class="conn-summary-detail" id="summary-claude">加载中...</span></div>
|
|
129
|
+
<div class="conn-summary-row"><span class="conn-summary-name">Codex</span><span class="conn-summary-detail" id="summary-codex">加载中...</span><span class="runtime-install-badge" id="rt-install-codex"></span></div>
|
|
130
|
+
<div class="conn-summary-row"><span class="conn-summary-name">Claude</span><span class="conn-summary-detail" id="summary-claude">加载中...</span><span class="runtime-install-badge" id="rt-install-claude"></span></div>
|
|
119
131
|
</div>
|
|
132
|
+
<div class="cred-state" id="rt-install-warning" aria-live="polite"></div>
|
|
120
133
|
<div class="settings-actions">
|
|
121
134
|
<button type="button" class="btn btn-outline" id="btn-switch-runtime">切换运行时</button>
|
|
122
135
|
<button type="button" class="btn btn-outline" id="btn-restart-session">重启 agent 会话</button>
|
|
@@ -239,6 +252,7 @@
|
|
|
239
252
|
<div class="settings-card">
|
|
240
253
|
<div class="card-title">运行时与认证 <span class="channel-state" id="diag-runtime-state"></span></div>
|
|
241
254
|
<div class="cred-state" id="diag-runtime" aria-live="polite">加载中...</div>
|
|
255
|
+
<div class="cred-state" id="diag-init-state" aria-live="polite"></div>
|
|
242
256
|
</div>
|
|
243
257
|
<div class="settings-card">
|
|
244
258
|
<div class="card-title">服务状态 <button type="button" class="small-btn" id="btn-diag-refresh">刷新</button></div>
|
|
@@ -328,6 +328,39 @@ function npmInstallDeps(cwd) {
|
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
/** Rebuild native modules after --ignore-scripts install (D44).
|
|
332
|
+
* --ignore-scripts skips postinstall, so better-sqlite3 etc. never compile
|
|
333
|
+
* their native binary. Rebuilding runs node-gyp only (no vendor lifecycle
|
|
334
|
+
* scripts) — the security stance is kept while the module becomes loadable. */
|
|
335
|
+
function rebuildNativeDeps(cwd) {
|
|
336
|
+
return new Promise((resolve) => {
|
|
337
|
+
const child = spawn('npm', ['rebuild', '--build-from-source', '--no-audit', '--no-fund'], {
|
|
338
|
+
cwd, env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'],
|
|
339
|
+
});
|
|
340
|
+
let stderr = '';
|
|
341
|
+
let settled = false;
|
|
342
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
343
|
+
const timer = setTimeout(() => {
|
|
344
|
+
if (settled) return;
|
|
345
|
+
settled = true;
|
|
346
|
+
child.kill('SIGTERM');
|
|
347
|
+
resolve({ ok: false, error: `npm rebuild 超时(${Math.round(NPM_INSTALL_TIMEOUT_MS / 1000)}s)` });
|
|
348
|
+
}, NPM_INSTALL_TIMEOUT_MS);
|
|
349
|
+
child.on('error', (err) => {
|
|
350
|
+
if (settled) return;
|
|
351
|
+
settled = true;
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
resolve({ ok: false, error: err.message });
|
|
354
|
+
});
|
|
355
|
+
child.on('close', (code) => {
|
|
356
|
+
if (settled) return;
|
|
357
|
+
settled = true;
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
resolve(code === 0 ? { ok: true } : { ok: false, error: (stderr.trim() || `exit ${code}`).slice(-500) });
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
331
364
|
/**
|
|
332
365
|
* Install @baize-ai/baize-a2a from an uploaded .tar.gz (D25 私有交付).
|
|
333
366
|
* Pipeline: entry listing + path-safety vetting (no `..`/absolute/links) →
|
|
@@ -342,6 +375,7 @@ export async function installA2aTarball(tarballPath, {
|
|
|
342
375
|
skillsDir = defaultSkillsDir(),
|
|
343
376
|
componentsFile: componentsPath = componentsFile(),
|
|
344
377
|
installDeps = npmInstallDeps,
|
|
378
|
+
rebuildDeps = rebuildNativeDeps,
|
|
345
379
|
originalName = null,
|
|
346
380
|
} = {}) {
|
|
347
381
|
const file = String(tarballPath || '');
|
|
@@ -401,6 +435,13 @@ export async function installA2aTarball(tarballPath, {
|
|
|
401
435
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
402
436
|
return { ok: false, error: `npm install 失败: ${depResult.error}` };
|
|
403
437
|
}
|
|
438
|
+
// D44: --ignore-scripts skipped native compilation — rebuild modules so
|
|
439
|
+
// better-sqlite3 etc. are loadable (node-gyp only, no vendor scripts).
|
|
440
|
+
const rebuildResult = await rebuildDeps(targetDir);
|
|
441
|
+
if (!rebuildResult.ok) {
|
|
442
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
443
|
+
return { ok: false, error: `native 模块重建失败: ${rebuildResult.error}` };
|
|
444
|
+
}
|
|
404
445
|
const components = readComponents(componentsPath);
|
|
405
446
|
components.a2a = {
|
|
406
447
|
version: pkg.version,
|
|
@@ -126,13 +126,57 @@ function readErrors() {
|
|
|
126
126
|
return errors;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** Init state from .baize/init-state.json (written by entrypoint/init). */
|
|
130
|
+
function readInitState() {
|
|
131
|
+
try {
|
|
132
|
+
const p = path.join(process.env.BAIZE_DIR || path.join(os.homedir(), 'baize'), '.baize', 'init-state.json');
|
|
133
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
134
|
+
} catch { return null; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Detect whether a runtime CLI is installed (PATH) + its version.
|
|
138
|
+
* Note: gate on `command -v` — a plain `<bin> --version | head -1` pipeline
|
|
139
|
+
* exits with head's status (0), so a missing binary would read as installed. */
|
|
140
|
+
function checkRuntimeBinary(bin) {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
execFile('sh', ['-c', `command -v ${bin} >/dev/null 2>&1 && ${bin} --version 2>/dev/null | head -1`], { timeout: 5000 }, (err, stdout) => {
|
|
143
|
+
resolve({ installed: !err, version: err ? null : (String(stdout).trim().split('\n')[0] || null) });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Read the D39 install state file (failure reason from an async web install). */
|
|
149
|
+
function readRuntimeInstallState(runtime) {
|
|
150
|
+
try {
|
|
151
|
+
const p = path.join(process.env.BAIZE_DIR || path.join(os.homedir(), 'baize'), '.baize', `runtime-install-${runtime}.json`);
|
|
152
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
153
|
+
} catch { return null; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Per-runtime install status: installed + version, plus the last install
|
|
158
|
+
* failure reason (D39 state file) so the console can tell the user exactly
|
|
159
|
+
* which runtime is missing and why.
|
|
160
|
+
*/
|
|
161
|
+
export async function readRuntimes() {
|
|
162
|
+
const [claude, codex] = await Promise.all([checkRuntimeBinary('claude'), checkRuntimeBinary('codex')]);
|
|
163
|
+
const enrich = (status, runtime) => {
|
|
164
|
+
const st = readRuntimeInstallState(runtime);
|
|
165
|
+
if (st && st.status === 'failed') status.installError = st.error || null;
|
|
166
|
+
return status;
|
|
167
|
+
};
|
|
168
|
+
return { claude: enrich(claude, 'claude'), codex: enrich(codex, 'codex') };
|
|
169
|
+
}
|
|
170
|
+
|
|
129
171
|
/** Full diagnostics payload. */
|
|
130
172
|
export async function readDiagnostics() {
|
|
131
|
-
const [services, sessions] = await Promise.all([readServices(), readSessions()]);
|
|
173
|
+
const [services, sessions, runtimes] = await Promise.all([readServices(), readSessions(), readRuntimes()]);
|
|
132
174
|
return {
|
|
133
175
|
success: true,
|
|
134
176
|
at: new Date().toISOString(),
|
|
135
177
|
runtime: readRuntime(),
|
|
178
|
+
init: readInitState(),
|
|
179
|
+
runtimes,
|
|
136
180
|
services,
|
|
137
181
|
sessions,
|
|
138
182
|
auth: readAuth(),
|
|
@@ -15,7 +15,7 @@ import crypto from 'crypto';
|
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import os from 'os';
|
|
17
17
|
import fs from 'fs';
|
|
18
|
-
import { spawn } from 'child_process';
|
|
18
|
+
import { spawn, execFile } from 'child_process';
|
|
19
19
|
import Database from 'better-sqlite3';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
21
|
import {
|
|
@@ -834,9 +834,122 @@ app.get('/api/admin/diagnostics', async (req, res) => {
|
|
|
834
834
|
}
|
|
835
835
|
});
|
|
836
836
|
|
|
837
|
+
// ── Runtime install (D39): async install when the target runtime binary is
|
|
838
|
+
// missing — detached spawn (900s timeout + 2 retries), then auto-switch.
|
|
839
|
+
// No task framework: a state JSON + log file are the status.
|
|
840
|
+
const RUNTIME_INSTALL = {
|
|
841
|
+
claude: { bin: 'claude', cmd: 'curl -fsSL https://claude.ai/install.sh | bash' },
|
|
842
|
+
codex: { bin: 'codex', cmd: 'npm install -g @openai/codex' },
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
function runtimeInstallStateFile(runtime) {
|
|
846
|
+
return path.join(BAIZE_DIR, '.baize', `runtime-install-${runtime}.json`);
|
|
847
|
+
}
|
|
848
|
+
function runtimeInstallLogFile(runtime) {
|
|
849
|
+
return path.join(BAIZE_DIR, '.baize', `runtime-install-${runtime}.log`);
|
|
850
|
+
}
|
|
851
|
+
function readRuntimeInstallState(runtime) {
|
|
852
|
+
try { return JSON.parse(fs.readFileSync(runtimeInstallStateFile(runtime), 'utf8')); } catch { return null; }
|
|
853
|
+
}
|
|
854
|
+
function writeRuntimeInstallState(runtime, state) {
|
|
855
|
+
try {
|
|
856
|
+
fs.mkdirSync(path.join(BAIZE_DIR, '.baize'), { recursive: true });
|
|
857
|
+
fs.writeFileSync(runtimeInstallStateFile(runtime), JSON.stringify(state));
|
|
858
|
+
} catch { /* non-fatal */ }
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function runtimeInstalled(runtime) {
|
|
862
|
+
const bin = RUNTIME_INSTALL[runtime]?.bin;
|
|
863
|
+
if (!bin) return Promise.resolve(true); // unknown runtime: don't gate
|
|
864
|
+
return new Promise((resolve) => {
|
|
865
|
+
execFile('sh', ['-c', `command -v ${bin}`], { timeout: 5000 }, (err) => resolve(!err));
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function startRuntimeInstall(runtime) {
|
|
870
|
+
const spec = RUNTIME_INSTALL[runtime];
|
|
871
|
+
if (!spec) return false;
|
|
872
|
+
const cur = readRuntimeInstallState(runtime);
|
|
873
|
+
if (cur && cur.status === 'installing') {
|
|
874
|
+
// Stale-guard: a crashed/killed installer (OOM, container restart) would
|
|
875
|
+
// otherwise wedge the target forever. Budget = 3 attempts x 900s + 240s
|
|
876
|
+
// switch + buffer.
|
|
877
|
+
const started = Date.parse(cur.startedAt || '');
|
|
878
|
+
if (Number.isNaN(started) || Date.now() - started < 55 * 60 * 1000) {
|
|
879
|
+
return false; // genuinely running (or recent) — don't duplicate
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
writeRuntimeInstallState(runtime, { status: 'installing', startedAt: new Date().toISOString() });
|
|
884
|
+
|
|
885
|
+
const script = `
|
|
886
|
+
const { spawn } = require('child_process');
|
|
887
|
+
const fs = require('fs');
|
|
888
|
+
const LOG = ${JSON.stringify(runtimeInstallLogFile(runtime))};
|
|
889
|
+
const STATE = ${JSON.stringify(runtimeInstallStateFile(runtime))};
|
|
890
|
+
const CMD = ${JSON.stringify(spec.cmd)};
|
|
891
|
+
const RUNTIME = ${JSON.stringify(runtime)};
|
|
892
|
+
const log = (s) => { try { fs.appendFileSync(LOG, s + '\\n'); } catch {} };
|
|
893
|
+
const run = (cmd, timeoutMs) => new Promise((resolve, reject) => {
|
|
894
|
+
const child = spawn('bash', ['-c', cmd], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
895
|
+
let out = '';
|
|
896
|
+
child.stdout.on('data', (d) => { out += d; if (out.length > 4000) { log(out); out = ''; } });
|
|
897
|
+
child.stderr.on('data', (d) => { out += d; if (out.length > 4000) { log(out); out = ''; } });
|
|
898
|
+
const killTree = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch {} } };
|
|
899
|
+
const t = setTimeout(() => { killTree(); reject(new Error('timeout ' + timeoutMs + 'ms')); }, timeoutMs);
|
|
900
|
+
child.on('close', (code) => { clearTimeout(t); if (out) log(out); code === 0 ? resolve() : reject(new Error('exit ' + code)); });
|
|
901
|
+
});
|
|
902
|
+
const update = (s) => { try { fs.writeFileSync(STATE, JSON.stringify(s)); } catch {} };
|
|
903
|
+
(async () => {
|
|
904
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
905
|
+
log('[install] attempt ' + attempt + ' starting: ' + CMD);
|
|
906
|
+
try {
|
|
907
|
+
await run(CMD, 900000);
|
|
908
|
+
log('[install] installed OK');
|
|
909
|
+
break;
|
|
910
|
+
} catch (e) {
|
|
911
|
+
log('[install] attempt ' + attempt + ' failed: ' + e.message);
|
|
912
|
+
if (attempt === 3) {
|
|
913
|
+
update({ status: 'failed', error: e.message, doneAt: new Date().toISOString() });
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
log('[install] switching runtime to ' + RUNTIME);
|
|
919
|
+
try {
|
|
920
|
+
await run('baize runtime ' + RUNTIME + ' --no-validate', 240000);
|
|
921
|
+
update({ status: 'done', doneAt: new Date().toISOString() });
|
|
922
|
+
log('[install] switch complete');
|
|
923
|
+
} catch (e) {
|
|
924
|
+
update({ status: 'failed', error: 'switch failed: ' + e.message, doneAt: new Date().toISOString() });
|
|
925
|
+
log('[install] switch failed: ' + e.message);
|
|
926
|
+
}
|
|
927
|
+
})();
|
|
928
|
+
`;
|
|
929
|
+
try {
|
|
930
|
+
const scriptPath = path.join(BAIZE_DIR, '.baize', `runtime-install-${runtime}.cjs`);
|
|
931
|
+
fs.mkdirSync(path.join(BAIZE_DIR, '.baize'), { recursive: true });
|
|
932
|
+
fs.writeFileSync(scriptPath, script);
|
|
933
|
+
const child = spawn(process.execPath, [scriptPath], { detached: true, stdio: 'ignore' });
|
|
934
|
+
child.unref();
|
|
935
|
+
return true;
|
|
936
|
+
} catch {
|
|
937
|
+
writeRuntimeInstallState(runtime, null); // don't leave a stuck 'installing'
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
837
942
|
app.post('/api/admin/runtime', async (req, res) => {
|
|
838
943
|
try {
|
|
839
|
-
const
|
|
944
|
+
const runtime = req.body?.runtime;
|
|
945
|
+
if (!['claude', 'codex'].includes(runtime)) {
|
|
946
|
+
return res.status(400).json({ success: false, error: `Invalid runtime: ${runtime}` });
|
|
947
|
+
}
|
|
948
|
+
if (!(await runtimeInstalled(runtime))) {
|
|
949
|
+
startRuntimeInstall(runtime);
|
|
950
|
+
return res.json({ success: true, installing: true, runtime });
|
|
951
|
+
}
|
|
952
|
+
const result = await switchRuntime(runtime);
|
|
840
953
|
if (result.success) result.restart = await restartAgentSession();
|
|
841
954
|
res.status(result.success ? 200 : 400).json(result);
|
|
842
955
|
} catch (err) {
|
|
@@ -844,6 +957,181 @@ app.post('/api/admin/runtime', async (req, res) => {
|
|
|
844
957
|
}
|
|
845
958
|
});
|
|
846
959
|
|
|
960
|
+
// ── Environment status + one-click re-init (D42) ───────────────────────────
|
|
961
|
+
// Web is the only recovery surface: detect init gaps, trigger the idempotent
|
|
962
|
+
// `baize init`, and surface failure reasons (human summary + raw log) in-browser.
|
|
963
|
+
const REINIT_LOG = path.join(BAIZE_DIR, '.baize', 'reinit.log');
|
|
964
|
+
const REINIT_STATE = path.join(BAIZE_DIR, '.baize', 'reinit.json');
|
|
965
|
+
|
|
966
|
+
function readReinitState() {
|
|
967
|
+
try { return JSON.parse(fs.readFileSync(REINIT_STATE, 'utf8')); } catch { return null; }
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function runtimeBinInstalled(bin) {
|
|
971
|
+
return new Promise((resolve) => {
|
|
972
|
+
execFile('sh', ['-c', `command -v ${bin} >/dev/null 2>&1`], { timeout: 5000 }, (err) => resolve(!err));
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Init completeness: config.json with a runtime. The instruction file may be
|
|
977
|
+
* in the split-instruction pending-migration state without blocking the
|
|
978
|
+
* session (Guardian still launches), so it is not a readiness condition. */
|
|
979
|
+
function initComplete() {
|
|
980
|
+
try {
|
|
981
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(BAIZE_DIR, '.baize', 'config.json'), 'utf8'));
|
|
982
|
+
return !!(cfg && cfg.runtime);
|
|
983
|
+
} catch { return false; }
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function readCurrentRuntime() {
|
|
987
|
+
try {
|
|
988
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(BAIZE_DIR, '.baize', 'config.json'), 'utf8'));
|
|
989
|
+
if (cfg && (cfg.runtime === 'codex' || cfg.runtime === 'claude')) return cfg.runtime;
|
|
990
|
+
} catch {}
|
|
991
|
+
return 'codex'; // mainland-safe default (init falls back to claude otherwise)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Map common failure text to a human-readable (Chinese) summary. */
|
|
995
|
+
function humanError(summary) {
|
|
996
|
+
const map = [
|
|
997
|
+
[/claude\.ai/i, '无法连接 Claude 官方服务(大陆不可达)——请使用 Codex 或第三方兼容端点'],
|
|
998
|
+
[/ETIMEDOUT|ECONNRESET|ECONNREFUSED|network.*(timeout|refused)|i\/o timeout/i, '网络连接失败(超时/被拒绝)——请检查网络后重试'],
|
|
999
|
+
[/registry\.npmjs\.org|npmmirror/i, '无法连接 npm 源——请检查网络或 npm 源配置'],
|
|
1000
|
+
[/Failed to install (Codex|claude)|Failed to install/i, '运行时 CLI 安装失败——请检查网络后重试'],
|
|
1001
|
+
[/permission denied|EACCES/i, '权限不足——请检查目录权限后重试'],
|
|
1002
|
+
[/command not found/i, '缺少必要命令——请检查环境'],
|
|
1003
|
+
];
|
|
1004
|
+
for (const [re, msg] of map) {
|
|
1005
|
+
if (re.test(summary)) return msg;
|
|
1006
|
+
}
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function startReinit() {
|
|
1011
|
+
const cur = readReinitState();
|
|
1012
|
+
if (cur && cur.status === 'running') {
|
|
1013
|
+
// Stale-guard: a crashed/killed reinit would otherwise wedge the button
|
|
1014
|
+
// forever. Budget = init install window (900s) + buffer.
|
|
1015
|
+
const started = Date.parse(cur.startedAt || '');
|
|
1016
|
+
if (Number.isNaN(started) || Date.now() - started < 30 * 60 * 1000) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const runtime = readCurrentRuntime();
|
|
1021
|
+
const now = new Date().toISOString();
|
|
1022
|
+
try {
|
|
1023
|
+
fs.mkdirSync(path.join(BAIZE_DIR, '.baize'), { recursive: true });
|
|
1024
|
+
fs.writeFileSync(REINIT_STATE, JSON.stringify({ status: 'running', runtime, startedAt: now }));
|
|
1025
|
+
fs.writeFileSync(REINIT_LOG, `[reinit] started at ${now} (runtime=${runtime})
|
|
1026
|
+
`);
|
|
1027
|
+
} catch { return false; }
|
|
1028
|
+
const script = `
|
|
1029
|
+
const { spawn } = require('child_process');
|
|
1030
|
+
const fs = require('fs');
|
|
1031
|
+
const LOG = ${JSON.stringify(REINIT_LOG)};
|
|
1032
|
+
const STATE = ${JSON.stringify(REINIT_STATE)};
|
|
1033
|
+
const RUNTIME = ${JSON.stringify(runtime)};
|
|
1034
|
+
const log = (s) => { try { fs.appendFileSync(LOG, s + '\\n'); } catch {} };
|
|
1035
|
+
const run = (cmd, timeoutMs) => new Promise((resolve, reject) => {
|
|
1036
|
+
const child = spawn('bash', ['-c', cmd], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1037
|
+
let out = '';
|
|
1038
|
+
child.stdout.on('data', (d) => { out += d; if (out.length > 8000) { log(out); out = ''; } });
|
|
1039
|
+
child.stderr.on('data', (d) => { out += d; if (out.length > 8000) { log(out); out = ''; } });
|
|
1040
|
+
const killTree = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch {} } };
|
|
1041
|
+
const t = setTimeout(() => { killTree(); reject(new Error('timeout ' + timeoutMs + 'ms')); }, timeoutMs);
|
|
1042
|
+
child.on('close', (code) => { clearTimeout(t); if (out) log(out); code === 0 ? resolve() : reject(new Error('exit ' + code)); });
|
|
1043
|
+
});
|
|
1044
|
+
const update = (s) => { try { fs.writeFileSync(STATE, JSON.stringify(s)); } catch {} };
|
|
1045
|
+
(async () => {
|
|
1046
|
+
log('[reinit] running baize init (idempotent)...');
|
|
1047
|
+
try {
|
|
1048
|
+
await run('baize init --yes --quiet --runtime ' + RUNTIME, 900000);
|
|
1049
|
+
log('[reinit] init OK');
|
|
1050
|
+
update({ status: 'done', doneAt: new Date().toISOString(), runtime: RUNTIME });
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
log('[reinit] init failed: ' + e.message);
|
|
1053
|
+
update({ status: 'failed', error: e.message, doneAt: new Date().toISOString(), runtime: RUNTIME });
|
|
1054
|
+
}
|
|
1055
|
+
})();
|
|
1056
|
+
`;
|
|
1057
|
+
try {
|
|
1058
|
+
const scriptPath = path.join(BAIZE_DIR, '.baize', 'reinit.cjs');
|
|
1059
|
+
fs.writeFileSync(scriptPath, script);
|
|
1060
|
+
const child = spawn(process.execPath, [scriptPath], { detached: true, stdio: 'ignore' });
|
|
1061
|
+
child.unref();
|
|
1062
|
+
return true;
|
|
1063
|
+
} catch {
|
|
1064
|
+
try { fs.writeFileSync(REINIT_STATE, JSON.stringify({ status: 'failed', error: '无法启动初始化进程', doneAt: new Date().toISOString() })); } catch {}
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Environment status: gaps in plain language + readiness.
|
|
1070
|
+
app.get('/api/admin/environment/status', async (req, res) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const [claude, codex] = await Promise.all([runtimeBinInstalled('claude'), runtimeBinInstalled('codex')]);
|
|
1073
|
+
const runtime = readCurrentRuntime();
|
|
1074
|
+
const initDone = initComplete();
|
|
1075
|
+
const gaps = [];
|
|
1076
|
+
if (!claude && !codex) gaps.push('运行时 CLI(Claude Code / Codex)均未安装');
|
|
1077
|
+
else if (runtime === 'claude' && !claude) gaps.push('当前运行时 Claude Code 未安装');
|
|
1078
|
+
else if (runtime === 'codex' && !codex) gaps.push('当前运行时 Codex 未安装');
|
|
1079
|
+
if (!initDone) gaps.push('环境初始化未完成(配置缺失)');
|
|
1080
|
+
res.json({
|
|
1081
|
+
success: true,
|
|
1082
|
+
runtime,
|
|
1083
|
+
runtimes: { claude: { installed: claude }, codex: { installed: codex } },
|
|
1084
|
+
initComplete: initDone,
|
|
1085
|
+
ready: (runtime === 'claude' ? claude : codex) && initDone,
|
|
1086
|
+
gaps,
|
|
1087
|
+
});
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
jsonError(res, err);
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
// Trigger the idempotent re-init (async; poll reinit-status).
|
|
1094
|
+
app.post('/api/admin/environment/reinit', (req, res) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const started = startReinit();
|
|
1097
|
+
res.json({ success: true, reinit: started });
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
jsonError(res, err);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Re-init progress/result + human-readable failure reason + log tail.
|
|
1104
|
+
app.get('/api/admin/environment/reinit-status', (req, res) => {
|
|
1105
|
+
try {
|
|
1106
|
+
const state = readReinitState();
|
|
1107
|
+
if (!state) return res.json({ success: true, status: 'idle' });
|
|
1108
|
+
let logTail = '';
|
|
1109
|
+
try { logTail = fs.readFileSync(REINIT_LOG, 'utf8').split('\n').filter(Boolean).slice(-20).join('\n'); } catch {}
|
|
1110
|
+
const human = state.status === 'failed' ? (humanError((state.error || '') + '\n' + logTail) || '初始化失败(详见日志)') : null;
|
|
1111
|
+
res.json({ success: true, status: state.status, error: state.error, human, logTail, runtime: state.runtime });
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
jsonError(res, err);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
app.get('/api/admin/runtime/install-status', (req, res) => {
|
|
1118
|
+
try {
|
|
1119
|
+
const runtime = req.query?.runtime;
|
|
1120
|
+
if (!['claude', 'codex'].includes(runtime)) {
|
|
1121
|
+
return res.status(400).json({ success: false, error: `Invalid runtime: ${runtime}` });
|
|
1122
|
+
}
|
|
1123
|
+
const state = readRuntimeInstallState(runtime);
|
|
1124
|
+
if (!state) return res.json({ success: true, status: 'idle' });
|
|
1125
|
+
let logTail = '';
|
|
1126
|
+
try {
|
|
1127
|
+
logTail = fs.readFileSync(runtimeInstallLogFile(runtime), 'utf8').split('\n').filter(Boolean).slice(-10).join('\n');
|
|
1128
|
+
} catch {}
|
|
1129
|
+
res.json({ success: true, status: state.status, error: state.error, logTail });
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
jsonError(res, err);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
|
|
847
1135
|
app.post('/api/admin/restart', async (req, res) => {
|
|
848
1136
|
try {
|
|
849
1137
|
res.json(await restartAgentSession());
|