@baize-ai/core 0.3.14 → 0.3.16
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/CHANGELOG.md +26 -0
- package/Dockerfile +10 -1
- package/cli/commands/add.js +3 -0
- package/cli/commands/component.js +289 -40
- package/cli/commands/init.js +91 -5
- package/cli/commands/runtime.js +9 -1
- package/cli/lib/__tests__/init-base-url.test.js +55 -13
- package/cli/lib/__tests__/runtime-base-url.test.js +3 -1
- package/cli/lib/__tests__/runtime-setup.test.js +53 -6
- package/cli/lib/runtime/codex.js +17 -0
- package/cli/lib/runtime-setup.js +153 -12
- package/cli/lib/upgrade.js +55 -15
- package/docker/entrypoint.sh +0 -20
- package/docker-compose.yml +20 -2
- package/package.json +2 -2
- package/scripts/docker-publish.sh +0 -4
- package/scripts/pack-release.sh +34 -26
- package/skills/web-console/public/app.js +44 -82
- package/skills/web-console/public/index.html +1 -1
- package/skills/web-console/scripts/a2a-admin.js +225 -26
- package/skills/web-console/scripts/model-provider.js +97 -59
- package/skills/web-console/scripts/server.js +22 -32
- package/templates/pm2/ecosystem.config.cjs +6 -0
- package/test/channel-admin.test.js +7 -5
- package/test/helpers/run-upgrade-file-driver.mjs +15 -0
- package/test/model-provider.test.js +150 -40
- package/test/upgrade-file.test.js +229 -0
- package/test/upgrade-local-version.test.js +70 -0
- package/test/upgrade-restart-hook.test.js +129 -0
- package/test/web-console-routes.test.js +94 -17
package/scripts/pack-release.sh
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# 兼容 web 控制台上传与 `baize add --file` 两种安装)
|
|
12
12
|
# baize-admin-workspace-<v>.tgz admin 套件源码:server 源码 + admin-ui 构建产物 +
|
|
13
13
|
# Dockerfile + 部署说明(自建镜像 / 自托管用)
|
|
14
|
-
# baize-admin-workspace-<v>.tar docker save 的 admin
|
|
14
|
+
# baize-admin-workspace-<v>.tar (可选)docker save 的 admin 镜像——仅 BUILD_ADMIN_DOCKER=1 打包时产出
|
|
15
15
|
# INSTALL.md 客户侧安装说明(上传 / --file 安装 + 切集群 + admin 部署)
|
|
16
16
|
#
|
|
17
17
|
# 用法:
|
|
@@ -155,8 +155,14 @@ build_admin_tgz() {
|
|
|
155
155
|
log " → ${out} ($(size "${out}"))"
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
# ── 3. Admin Docker 镜像(docker save
|
|
158
|
+
# ── 3. Admin Docker 镜像(docker save)——可选 ─────────────────────────────
|
|
159
|
+
# 商业版 admin 默认源码交付(tgz 含 server + 已构建 admin-ui/dist)。
|
|
160
|
+
# 仅当 BUILD_ADMIN_DOCKER=1 时才导出 docker save 镜像(需先构建 baize-admin-workspace:test)。
|
|
159
161
|
save_admin_image() {
|
|
162
|
+
if [ "${BUILD_ADMIN_DOCKER:-0}" != "1" ]; then
|
|
163
|
+
step "跳过 admin Docker 镜像导出(源码交付模式;需要时 BUILD_ADMIN_DOCKER=1 重跑)"
|
|
164
|
+
return 0
|
|
165
|
+
fi
|
|
160
166
|
step "导出 admin Docker 镜像"
|
|
161
167
|
local image="baize-admin-workspace:test"
|
|
162
168
|
if ! docker image inspect "${image}" >/dev/null 2>&1; then
|
|
@@ -231,36 +237,35 @@ baize a2a status # 期望:集群·在线
|
|
|
231
237
|
```bash
|
|
232
238
|
baize a2a disable # 停 daemon,保留配置与注册数据,模式回 standalone
|
|
233
239
|
```
|
|
240
|
+
### 方式 A:源码部署(推荐——商业版默认交付形态)
|
|
234
241
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
### 方式 A:Docker 镜像(推荐)
|
|
242
|
+
套件已含 server 源码与**构建好的** `admin-ui/dist`,无需前端构建:
|
|
238
243
|
|
|
239
244
|
```bash
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
baize-admin
|
|
245
|
+
tar xzf baize-admin-workspace-__VERSION__.tgz
|
|
246
|
+
cd baize-admin-workspace-__VERSION__
|
|
247
|
+
npm ci --omit=dev # Node ≥ 20.11
|
|
248
|
+
BAIZE_ADMIN_TOKEN='<初始口令>' BAIZE_ADMIN_DIR=/srv/baize-admin \
|
|
249
|
+
pm2 start server/src/index.js --name baize-admin # 或 nohup node server/src/index.js
|
|
245
250
|
```
|
|
246
251
|
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
252
|
+
- API 监听 8080;`BAIZE_ADMIN_DIR` 是数据目录(SQLite/CA/签名钥),必须持久化并提前建目录;
|
|
253
|
+
- 前端 `admin-ui/dist` 是静态产物:用 nginx/Caddy 反代到与 API 同源的根路径;
|
|
254
|
+
- 公网部署必须 `TRUST_PROXY=1`(信任反代 X-Forwarded-For,否则限流按代理 IP 计数);
|
|
255
|
+
- 首次登录 `admin` + 初始口令(日志打印),登录后到「设置」改口令。
|
|
250
256
|
|
|
251
|
-
### 方式 B
|
|
257
|
+
### 方式 B:Docker(可选——需自建镜像)
|
|
252
258
|
|
|
253
259
|
```bash
|
|
254
|
-
tar xzf baize-admin-workspace-__VERSION__.tgz
|
|
255
|
-
cd baize-admin-workspace-__VERSION__
|
|
256
260
|
docker build -t baize-admin-workspace . # 多阶段:node:22 构建 admin-ui → node:22-slim 运行
|
|
257
|
-
docker run -d --name baize-admin -p 8080:8080
|
|
261
|
+
docker run -d --name baize-admin -p 8080:8080 \
|
|
262
|
+
-v baize-admin-data:/data -e BAIZE_ADMIN_TOKEN='<初始口令>' baize-admin-workspace
|
|
258
263
|
```
|
|
259
264
|
|
|
260
|
-
|
|
261
|
-
- 前端构建产物已含在 `admin-ui/dist`(容器内 `/app/dist`),可与 API 同源静态提供。
|
|
265
|
+
> 厂商如需分发 Docker 镜像 tar:打包时 `BUILD_ADMIN_DOCKER=1 ./scripts/pack-release.sh`(本默认交付不包含 tar)。
|
|
262
266
|
|
|
263
|
-
|
|
267
|
+
- 前端构建产物已含在 `admin-ui/dist`,与 API 同源静态提供(nginx/Caddy 指到该目录即可)。
|
|
268
|
+
- API 默认 8080(`PORT` 可改);本地直跑:`BAIZE_ADMIN_DIR=/srv/baize-admin npm start`。
|
|
264
269
|
|
|
265
270
|
```bash
|
|
266
271
|
baize a2a disable # 先切回单机 / 停服务
|
|
@@ -332,13 +337,16 @@ verify() {
|
|
|
332
337
|
echo " !! ${admin_tgz} 校验失败(解压或 manifest 不正确)" >&2
|
|
333
338
|
rc=1
|
|
334
339
|
fi
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
340
|
+
# docker save 产物:可选(BUILD_ADMIN_DOCKER=1 打包时才存在)
|
|
341
|
+
if [ -e "${admin_tar}" ] || [ "${BUILD_ADMIN_DOCKER:-0}" = "1" ]; then
|
|
342
|
+
if [ -s "${admin_tar}" ]; then
|
|
343
|
+
log " OK ${admin_tar} ($(size "${admin_tar}")) 存在且非空"
|
|
344
|
+
else
|
|
345
|
+
echo " !! ${admin_tar} 不存在或为空" >&2
|
|
346
|
+
rc=1
|
|
347
|
+
fi
|
|
339
348
|
else
|
|
340
|
-
|
|
341
|
-
rc=1
|
|
349
|
+
log " -- ${admin_tar##*/} 未打包(源码交付模式,BUILD_ADMIN_DOCKER=1 可启用)"
|
|
342
350
|
fi
|
|
343
351
|
|
|
344
352
|
[ -f "${DIST_DIR}/INSTALL.md" ] && log " OK ${DIST_DIR}/INSTALL.md ($(size "${DIST_DIR}/INSTALL.md"))"
|
|
@@ -1170,7 +1170,18 @@ async function loadProviders(basePath) {
|
|
|
1170
1170
|
const names = { official: 'Anthropic 官方', 'official-openai': 'OpenAI 官方' };
|
|
1171
1171
|
const prov = (body.providers || []).find((x) => x.id === body.active);
|
|
1172
1172
|
const label = prov?.name || names[body.active] || body.active || 'official';
|
|
1173
|
-
|
|
1173
|
+
const codexSlug = body.codexProviderKey || prov?.codex?.providerKey || null;
|
|
1174
|
+
const codexKind = prov?.codex?.kind || null;
|
|
1175
|
+
// D51 review P3: surface the kind so openai-official (no block) vs
|
|
1176
|
+
// responses-compatible (provider block) are distinguishable at a glance.
|
|
1177
|
+
const codexTag = codexKind === 'responses-compatible' && codexSlug
|
|
1178
|
+
? `(codex: ${codexSlug} · responses)`
|
|
1179
|
+
: codexKind === 'openai-official'
|
|
1180
|
+
? '(codex: OpenAI 官方)'
|
|
1181
|
+
: codexSlug
|
|
1182
|
+
? `(codex: ${codexSlug})`
|
|
1183
|
+
: '';
|
|
1184
|
+
activeEl.textContent = `当前激活:${label}${codexTag}`;
|
|
1174
1185
|
}
|
|
1175
1186
|
const list = document.getElementById('provider-list');
|
|
1176
1187
|
if (!list) return;
|
|
@@ -1249,7 +1260,8 @@ async function activateProviderFromAdmin(basePath, id) {
|
|
|
1249
1260
|
const restartNote = body.restart?.restarted ? ',会话已重启' : (body.restart?.reason === 'no_session' ? '(无运行中会话)' : '');
|
|
1250
1261
|
const warnNote = body.warnings?.length ? `
|
|
1251
1262
|
警告:${body.warnings.join('\n')}` : '';
|
|
1252
|
-
|
|
1263
|
+
const slugNote = body.codexProviderKey ? `(codex: ${body.codexProviderKey})` : '';
|
|
1264
|
+
setAdminMsg(`已激活 ${body.active}${slugNote}${restartNote}${warnNote}`);
|
|
1253
1265
|
} else {
|
|
1254
1266
|
setAdminMsg(`激活失败:${body.error || '未知错误'}`, true);
|
|
1255
1267
|
}
|
|
@@ -1719,10 +1731,10 @@ function updateA2aInstallArea(body) {
|
|
|
1719
1731
|
if (!el || !fileInput || !btn) return;
|
|
1720
1732
|
const version = body && (body.version || body.installedVersion);
|
|
1721
1733
|
if (version) {
|
|
1722
|
-
el.textContent = `已安装 baize-a2a v${String(version).replace(/^v/, '')}
|
|
1734
|
+
el.textContent = `已安装 baize-a2a v${String(version).replace(/^v/, '')} — 上传更高版本的模块包(.tgz)可在线升级(相同或更低版本将被拒绝)。`;
|
|
1723
1735
|
if (state) { state.textContent = '已安装'; state.className = 'channel-state ok'; }
|
|
1724
|
-
fileInput.disabled =
|
|
1725
|
-
btn.disabled =
|
|
1736
|
+
fileInput.disabled = false;
|
|
1737
|
+
btn.disabled = false;
|
|
1726
1738
|
} else {
|
|
1727
1739
|
el.textContent = '未安装 — 上传 baize-a2a 模块包(.tgz)完成安装。';
|
|
1728
1740
|
if (state) { state.textContent = '未安装'; state.className = 'channel-state warn'; }
|
|
@@ -1782,11 +1794,16 @@ function updateA2aHealth(body, peerCountOverride) {
|
|
|
1782
1794
|
} else if (_lastA2aPeerCount !== null) {
|
|
1783
1795
|
peerCount = _lastA2aPeerCount;
|
|
1784
1796
|
}
|
|
1797
|
+
const bs = body && body.bootstrap ? body.bootstrap : null;
|
|
1798
|
+
const bsLabel = !bs ? '' : (bs.status === 'ok' || bs.status === 'launched'
|
|
1799
|
+
? `启动中/已启动(${escapeHtml(String(bs.via || bs.status))})`
|
|
1800
|
+
: `失败:${escapeHtml(String(bs.error || bs.status))}`);
|
|
1785
1801
|
const rows = [
|
|
1786
1802
|
['Daemon 健康', healthy ? '✅ 正常' : '❌ 异常'],
|
|
1787
1803
|
['运行模式', modeLabel],
|
|
1788
1804
|
['Peer 数', peerCount],
|
|
1789
1805
|
['最近心跳', a2aHeartbeatLabel(body)],
|
|
1806
|
+
...(bsLabel ? [['init 自启动', bsLabel]] : []),
|
|
1790
1807
|
].filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
1791
1808
|
el.innerHTML = rows.map(([k, v]) => `<div class="conn-summary-row"><span class="conn-summary-name">${escapeHtml(k)}</span><span class="conn-summary-detail">${escapeHtml(String(v))}</span></div>`).join('');
|
|
1792
1809
|
if (state) {
|
|
@@ -1858,13 +1875,11 @@ async function openAgentCardEditor(basePath) {
|
|
|
1858
1875
|
let catalogue;
|
|
1859
1876
|
let card;
|
|
1860
1877
|
let authz;
|
|
1861
|
-
let peerIds;
|
|
1862
1878
|
try {
|
|
1863
|
-
const [catRes, cardRes, authzRes
|
|
1879
|
+
const [catRes, cardRes, authzRes] = await Promise.all([
|
|
1864
1880
|
adminFetch(basePath, '/api/admin/a2a/card/skills'),
|
|
1865
1881
|
adminFetch(basePath, '/api/admin/a2a/card'),
|
|
1866
1882
|
adminFetch(basePath, '/api/admin/a2a/authz'),
|
|
1867
|
-
adminFetch(basePath, '/api/admin/a2a/peers'),
|
|
1868
1883
|
]);
|
|
1869
1884
|
if (catRes.status !== 200) throw new Error(catRes.body.error || 'skills 列表加载失败');
|
|
1870
1885
|
if (cardRes.status !== 200) throw new Error(cardRes.body.error || '名片加载失败');
|
|
@@ -1872,9 +1887,6 @@ async function openAgentCardEditor(basePath) {
|
|
|
1872
1887
|
catalogue = catRes.body.skills || [];
|
|
1873
1888
|
card = cardRes.body;
|
|
1874
1889
|
authz = authzRes.body;
|
|
1875
|
-
peerIds = peersRes.status === 200 && Array.isArray(peersRes.body.peers)
|
|
1876
|
-
? peersRes.body.peers.map((p) => p.agentId).filter(Boolean)
|
|
1877
|
-
: [];
|
|
1878
1890
|
} catch (err) {
|
|
1879
1891
|
setAdminMsg(`❌ 无法打开名片编辑器:${err.message}`, true);
|
|
1880
1892
|
return;
|
|
@@ -1912,14 +1924,7 @@ async function openAgentCardEditor(basePath) {
|
|
|
1912
1924
|
<label class="authz-mode-option"><input type="radio" name="authz-mode" value="allowlist"> 白名单</label>
|
|
1913
1925
|
</div>
|
|
1914
1926
|
<p class="conn-block-desc" id="authz-mode-desc"></p>
|
|
1915
|
-
<
|
|
1916
|
-
<div class="authz-tags" id="authz-tags" aria-live="polite"></div>
|
|
1917
|
-
<div class="authz-add-row">
|
|
1918
|
-
<input type="text" id="authz-peer-input" class="agent-card-input" placeholder="输入 peer agent_id 后添加" autocomplete="off" spellcheck="false">
|
|
1919
|
-
<button type="button" class="btn btn-outline" id="authz-peer-add">添加</button>
|
|
1920
|
-
</div>
|
|
1921
|
-
<div class="agent-card-field-label">从 peer 列表选择</div>
|
|
1922
|
-
<select id="authz-peer-select" class="agent-card-input" aria-label="从 peer 列表选择"></select>
|
|
1927
|
+
<p class="conn-block-desc" style="color:#8c8c8c">允许 / 阻止名单由 admin 控制台统一管控(组织架构 · 调用白名单),此处仅切换本 agent 的授权模式。</p>
|
|
1923
1928
|
</div>
|
|
1924
1929
|
</div>
|
|
1925
1930
|
<div class="agent-card-modal-foot">
|
|
@@ -1938,18 +1943,10 @@ async function openAgentCardEditor(basePath) {
|
|
|
1938
1943
|
nameInput.value = card.name || '';
|
|
1939
1944
|
descInput.value = card.description || '';
|
|
1940
1945
|
|
|
1941
|
-
// ── 调用授权 tab state (
|
|
1942
|
-
const allowSet = new Set(Array.isArray(authz.allow) ? authz.allow : []);
|
|
1943
|
-
const blockSet = new Set(Array.isArray(authz.block) ? authz.block : []);
|
|
1946
|
+
// ── 调用授权 tab state (D52: local mode only; lists are admin-managed) ──
|
|
1944
1947
|
let authzMode = authz.mode === 'allowlist' ? 'allowlist' : 'open';
|
|
1945
|
-
const activeAuthzList = () => (authzMode === 'allowlist' ? allowSet : blockSet);
|
|
1946
1948
|
|
|
1947
1949
|
const authzModeDesc = overlay.querySelector('#authz-mode-desc');
|
|
1948
|
-
const authzListTitle = overlay.querySelector('#authz-list-title');
|
|
1949
|
-
const authzCount = overlay.querySelector('#authz-count');
|
|
1950
|
-
const authzTagsEl = overlay.querySelector('#authz-tags');
|
|
1951
|
-
const authzPeerInput = overlay.querySelector('#authz-peer-input');
|
|
1952
|
-
const authzPeerSelect = overlay.querySelector('#authz-peer-select');
|
|
1953
1950
|
|
|
1954
1951
|
const renderAuthz = () => {
|
|
1955
1952
|
overlay.querySelectorAll('input[name="authz-mode"]').forEach((r) => {
|
|
@@ -1957,19 +1954,8 @@ async function openAgentCardEditor(basePath) {
|
|
|
1957
1954
|
});
|
|
1958
1955
|
const isAllowlist = authzMode === 'allowlist';
|
|
1959
1956
|
authzModeDesc.textContent = isAllowlist
|
|
1960
|
-
? '
|
|
1961
|
-
: '接受所有已批准 agent
|
|
1962
|
-
authzListTitle.textContent = isAllowlist ? '允许列表' : '阻止列表';
|
|
1963
|
-
const list = [...activeAuthzList()];
|
|
1964
|
-
authzCount.textContent = `${list.length} 个`;
|
|
1965
|
-
authzTagsEl.innerHTML = list.length
|
|
1966
|
-
? list.map((id) => `<span class="authz-tag">${escapeHtml(id)}<button type="button" class="authz-tag-x" aria-label="移除 ${escapeHtml(id)}" data-id="${escapeHtml(id)}">×</button></span>`).join('')
|
|
1967
|
-
: '<span class="cred-state">(空)</span>';
|
|
1968
|
-
const current = new Set(activeAuthzList());
|
|
1969
|
-
const available = peerIds.filter((id) => !current.has(id));
|
|
1970
|
-
authzPeerSelect.innerHTML = `<option value="">${available.length ? '选择 peer 添加到列表…' : '列表已包含所有已知 peer'}</option>`
|
|
1971
|
-
+ available.map((id) => `<option value="${escapeHtml(id)}">${escapeHtml(id)}</option>`).join('');
|
|
1972
|
-
authzPeerSelect.disabled = available.length === 0;
|
|
1957
|
+
? '仅允许白名单中的 agent 调用本 agent(白名单由 admin 控制台配置;空名单 = 无人可调用)。'
|
|
1958
|
+
: '接受所有已批准 agent(除 admin 控制台配置的阻止名单)。';
|
|
1973
1959
|
};
|
|
1974
1960
|
|
|
1975
1961
|
overlay.querySelectorAll('input[name="authz-mode"]').forEach((r) => {
|
|
@@ -1981,42 +1967,6 @@ async function openAgentCardEditor(basePath) {
|
|
|
1981
1967
|
});
|
|
1982
1968
|
});
|
|
1983
1969
|
|
|
1984
|
-
const addAuthzPeer = () => {
|
|
1985
|
-
const value = authzPeerInput.value.trim();
|
|
1986
|
-
if (!value) {
|
|
1987
|
-
setAdminMsg('请输入 peer agent_id', true);
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
const list = activeAuthzList();
|
|
1991
|
-
if (list.has(value)) {
|
|
1992
|
-
setAdminMsg(`⚠️ ${value} 已在${authzMode === 'allowlist' ? '允许' : '阻止'}列表中`, true);
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
list.add(value);
|
|
1996
|
-
authzPeerInput.value = '';
|
|
1997
|
-
renderAuthz();
|
|
1998
|
-
};
|
|
1999
|
-
authzPeerInput.addEventListener('keydown', (e) => {
|
|
2000
|
-
if (e.key === 'Enter') {
|
|
2001
|
-
e.preventDefault();
|
|
2002
|
-
addAuthzPeer();
|
|
2003
|
-
}
|
|
2004
|
-
});
|
|
2005
|
-
overlay.querySelector('#authz-peer-add').addEventListener('click', addAuthzPeer);
|
|
2006
|
-
authzPeerSelect.addEventListener('change', () => {
|
|
2007
|
-
const value = authzPeerSelect.value;
|
|
2008
|
-
if (!value) return;
|
|
2009
|
-
activeAuthzList().add(value);
|
|
2010
|
-
renderAuthz();
|
|
2011
|
-
});
|
|
2012
|
-
authzTagsEl.addEventListener('click', (e) => {
|
|
2013
|
-
const btn = e.target.closest('.authz-tag-x');
|
|
2014
|
-
if (btn) {
|
|
2015
|
-
activeAuthzList().delete(btn.dataset.id);
|
|
2016
|
-
renderAuthz();
|
|
2017
|
-
}
|
|
2018
|
-
});
|
|
2019
|
-
|
|
2020
1970
|
// ── Tab switching ──
|
|
2021
1971
|
const tabButtons = overlay.querySelectorAll('.agent-card-tab');
|
|
2022
1972
|
const cardTabEl = overlay.querySelector('#agent-card-tab-card');
|
|
@@ -2118,7 +2068,7 @@ async function openAgentCardEditor(basePath) {
|
|
|
2118
2068
|
adminFetch(basePath, '/api/admin/a2a/authz', {
|
|
2119
2069
|
method: 'PUT',
|
|
2120
2070
|
headers: { 'Content-Type': 'application/json' },
|
|
2121
|
-
body: JSON.stringify({ mode: authzMode
|
|
2071
|
+
body: JSON.stringify({ mode: authzMode }),
|
|
2122
2072
|
}),
|
|
2123
2073
|
]);
|
|
2124
2074
|
if (cardRes.status === 401 || authzRes.status === 401) {
|
|
@@ -2282,7 +2232,7 @@ async function submitA2aInstall(basePath, btn) {
|
|
|
2282
2232
|
}
|
|
2283
2233
|
const original = btn.textContent;
|
|
2284
2234
|
btn.disabled = true;
|
|
2285
|
-
btn.textContent = '
|
|
2235
|
+
btn.textContent = '上传处理中...';
|
|
2286
2236
|
feedback.textContent = '';
|
|
2287
2237
|
feedback.className = '';
|
|
2288
2238
|
const fd = new FormData();
|
|
@@ -2291,11 +2241,23 @@ async function submitA2aInstall(basePath, btn) {
|
|
|
2291
2241
|
const { status, body } = await adminFetch(basePath, '/api/admin/a2a/install', { method: 'POST', body: fd });
|
|
2292
2242
|
if (body.ok === true || body.success === true) {
|
|
2293
2243
|
const version = body.version ? ` v${String(body.version).replace(/^v/, '')}` : '';
|
|
2294
|
-
|
|
2244
|
+
const verb = body.upgraded ? '升级' : '安装';
|
|
2245
|
+
let okMsg = `✅ ${verb}成功${version}`;
|
|
2246
|
+
if (body.upgraded && body.restarted === false) {
|
|
2247
|
+
okMsg += `(⚠️ daemon 重启失败:${body.restartError || '未知原因'},请检查状态卡或手动重启)`;
|
|
2248
|
+
}
|
|
2249
|
+
feedback.textContent = okMsg;
|
|
2295
2250
|
feedback.className = 'a2a-feedback-ok';
|
|
2296
2251
|
fileInput.value = '';
|
|
2297
|
-
setAdminMsg(`✅ baize-a2a
|
|
2298
|
-
loadA2aStatus(basePath); //
|
|
2252
|
+
setAdminMsg(`✅ baize-a2a ${verb}成功${version}`);
|
|
2253
|
+
loadA2aStatus(basePath); // 刷新版本/模式/健康/安装卡
|
|
2254
|
+
} else if (body.error === 'version_not_higher') {
|
|
2255
|
+
const local = body.local ? `v${String(body.local).replace(/^v/, '')}` : '未知';
|
|
2256
|
+
const incoming = body.incoming ? `v${String(body.incoming).replace(/^v/, '')}` : '未知';
|
|
2257
|
+
const msg = `上传版本 ${incoming} 不高于已安装版本 ${local},未执行升级`;
|
|
2258
|
+
feedback.textContent = `❌ 升级被拒绝:${msg}`;
|
|
2259
|
+
feedback.className = 'a2a-feedback-error';
|
|
2260
|
+
setAdminMsg(`❌ 升级被拒绝:${msg}`, true);
|
|
2299
2261
|
} else {
|
|
2300
2262
|
const msg = body.error || body.message || `HTTP ${status}`;
|
|
2301
2263
|
feedback.textContent = `❌ 安装失败:${msg}`;
|
|
@@ -323,7 +323,7 @@
|
|
|
323
323
|
<div class="cred-state" id="a2a-install-status" role="status" aria-live="polite">加载中...</div>
|
|
324
324
|
<form id="form-a2a-install" class="settings-form">
|
|
325
325
|
<input type="file" id="a2a-install-file" accept=".tgz,.tar.gz,.zip" aria-label="选择 baize-a2a 模块包">
|
|
326
|
-
<button type="submit" class="btn btn-primary" id="btn-a2a-install"
|
|
326
|
+
<button type="submit" class="btn btn-primary" id="btn-a2a-install">上传安装/升级</button>
|
|
327
327
|
</form>
|
|
328
328
|
<div class="cred-state" id="a2a-install-feedback" role="status" aria-live="polite"></div>
|
|
329
329
|
</div>
|
|
@@ -362,63 +362,76 @@ function rebuildNativeDeps(cwd) {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* manifest check (name = @baize-ai/baize-a2a, version present) → extract to
|
|
368
|
-
* SKILLS_DIR/a2a → npm install --omit=dev → components.json registration.
|
|
369
|
-
* Already installed → { ok:false, error } naming the installed version
|
|
370
|
-
* (mirrors `baize add`; version bumps go through `baize upgrade a2a`).
|
|
365
|
+
* Shared upload vetting for install/upgrade (D25/D49): extension → size →
|
|
366
|
+
* entry listing → path-traversal scan → link scan → package manifest.
|
|
371
367
|
*
|
|
372
|
-
* @returns {
|
|
368
|
+
* @returns {{ error: string } | { pkg: object, entries: string[] }}
|
|
373
369
|
*/
|
|
374
|
-
|
|
375
|
-
skillsDir = defaultSkillsDir(),
|
|
376
|
-
componentsFile: componentsPath = componentsFile(),
|
|
377
|
-
installDeps = npmInstallDeps,
|
|
378
|
-
rebuildDeps = rebuildNativeDeps,
|
|
379
|
-
originalName = null,
|
|
380
|
-
} = {}) {
|
|
381
|
-
const file = String(tarballPath || '');
|
|
370
|
+
function validateA2aTarball(file, originalName) {
|
|
382
371
|
const nameForType = String(originalName || file || '');
|
|
383
372
|
if (!/\.(?:tar\.gz|tgz)$/i.test(nameForType)) {
|
|
384
|
-
return {
|
|
373
|
+
return { error: '安装包必须是 .tar.gz 归档' };
|
|
385
374
|
}
|
|
386
375
|
let stat;
|
|
387
376
|
try {
|
|
388
377
|
stat = fs.statSync(file);
|
|
389
378
|
} catch {
|
|
390
|
-
return {
|
|
379
|
+
return { error: '上传文件不存在或不可读' };
|
|
391
380
|
}
|
|
392
381
|
if (stat.size > A2A_INSTALL_MAX_BYTES) {
|
|
393
|
-
return {
|
|
382
|
+
return { error: `文件超过 ${A2A_INSTALL_MAX_MB}MB 限制` };
|
|
394
383
|
}
|
|
395
384
|
let entries;
|
|
396
385
|
try {
|
|
397
386
|
entries = listTarballEntries(file);
|
|
398
387
|
} catch (err) {
|
|
399
|
-
return {
|
|
388
|
+
return { error: `无法读取归档: ${err.message}` };
|
|
400
389
|
}
|
|
401
|
-
if (entries.length === 0) return {
|
|
390
|
+
if (entries.length === 0) return { error: '归档为空' };
|
|
402
391
|
const unsafe = entries.find(isUnsafeArchiveEntry);
|
|
403
|
-
if (unsafe) return {
|
|
392
|
+
if (unsafe) return { error: `归档包含不安全的路径条目: ${unsafe}` };
|
|
404
393
|
try {
|
|
405
394
|
assertNoArchiveLinks(file);
|
|
406
395
|
} catch (err) {
|
|
407
|
-
return {
|
|
396
|
+
return { error: err.message };
|
|
408
397
|
}
|
|
409
398
|
let pkg;
|
|
410
399
|
try {
|
|
411
400
|
pkg = readTarballPackageJson(file, entries);
|
|
412
401
|
} catch (err) {
|
|
413
|
-
return {
|
|
402
|
+
return { error: `manifest 解析失败: ${err.message}` };
|
|
414
403
|
}
|
|
415
|
-
if (!pkg) return {
|
|
404
|
+
if (!pkg) return { error: '归档缺少 package.json' };
|
|
416
405
|
if (pkg.name !== A2A_PACKAGE_NAME) {
|
|
417
|
-
return {
|
|
406
|
+
return { error: `manifest name 不符: 期望 ${A2A_PACKAGE_NAME},实际 ${pkg.name}` };
|
|
418
407
|
}
|
|
419
408
|
if (typeof pkg.version !== 'string' || !pkg.version.trim()) {
|
|
420
|
-
return {
|
|
409
|
+
return { error: 'manifest 缺少 version' };
|
|
421
410
|
}
|
|
411
|
+
return { pkg, entries };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Install @baize-ai/baize-a2a from an uploaded .tar.gz (D25 私有交付).
|
|
416
|
+
* Pipeline: entry listing + path-safety vetting (no `..`/absolute/links) →
|
|
417
|
+
* manifest check (name = @baize-ai/baize-a2a, version present) → extract to
|
|
418
|
+
* SKILLS_DIR/a2a → npm install --omit=dev → components.json registration.
|
|
419
|
+
* Already installed → { ok:false, error } naming the installed version
|
|
420
|
+
* (the web route dispatches to upgradeA2aTarball instead — D49 K3).
|
|
421
|
+
*
|
|
422
|
+
* @returns {Promise<{ok:boolean, version?:string, error?:string}>}
|
|
423
|
+
*/
|
|
424
|
+
export async function installA2aTarball(tarballPath, {
|
|
425
|
+
skillsDir = defaultSkillsDir(),
|
|
426
|
+
componentsFile: componentsPath = componentsFile(),
|
|
427
|
+
installDeps = npmInstallDeps,
|
|
428
|
+
rebuildDeps = rebuildNativeDeps,
|
|
429
|
+
originalName = null,
|
|
430
|
+
} = {}) {
|
|
431
|
+
const file = String(tarballPath || '');
|
|
432
|
+
const verdict = validateA2aTarball(file, originalName);
|
|
433
|
+
if (verdict.error) return { ok: false, error: verdict.error };
|
|
434
|
+
const { pkg, entries } = verdict;
|
|
422
435
|
const targetDir = path.join(skillsDir, 'a2a');
|
|
423
436
|
const installed = readComponents(componentsPath).a2a;
|
|
424
437
|
if (fs.existsSync(targetDir) || installed) {
|
|
@@ -456,6 +469,175 @@ export async function installA2aTarball(tarballPath, {
|
|
|
456
469
|
return { ok: true, version: pkg.version };
|
|
457
470
|
}
|
|
458
471
|
|
|
472
|
+
/** Semver compare (no deps; returns -1|0|1, null when unparseable).
|
|
473
|
+
* Prerelease ranks below release; prerelease identifiers compared
|
|
474
|
+
* numerically when numeric, lexically otherwise. */
|
|
475
|
+
export function compareA2aSemver(a, b) {
|
|
476
|
+
const parse = (v) => String(v || '').trim().replace(/^v/, '')
|
|
477
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
478
|
+
const pa = parse(a);
|
|
479
|
+
const pb = parse(b);
|
|
480
|
+
if (!pa || !pb) return null;
|
|
481
|
+
for (const i of [1, 2, 3]) {
|
|
482
|
+
const av = Number(pa[i]);
|
|
483
|
+
const bv = Number(pb[i]);
|
|
484
|
+
if (av !== bv) return av < bv ? -1 : 1;
|
|
485
|
+
}
|
|
486
|
+
if (pa[4] === pb[4]) return 0;
|
|
487
|
+
if (!pa[4]) return 1;
|
|
488
|
+
if (!pb[4]) return -1;
|
|
489
|
+
const aParts = pa[4].split('.');
|
|
490
|
+
const bParts = pb[4].split('.');
|
|
491
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
492
|
+
const av = aParts[i];
|
|
493
|
+
const bv = bParts[i];
|
|
494
|
+
if (av === bv) continue;
|
|
495
|
+
if (av === undefined) return -1;
|
|
496
|
+
if (bv === undefined) return 1;
|
|
497
|
+
const an = /^\d+$/.test(av) ? Number(av) : null;
|
|
498
|
+
const bn = /^\d+$/.test(bv) ? Number(bv) : null;
|
|
499
|
+
if (an !== null && bn !== null) return an < bn ? -1 : 1;
|
|
500
|
+
if (an !== null) return -1;
|
|
501
|
+
if (bn !== null) return 1;
|
|
502
|
+
return av < bv ? -1 : 1;
|
|
503
|
+
}
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function readInstalledPkgVersion(skillDir) {
|
|
508
|
+
try {
|
|
509
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(skillDir, 'package.json'), 'utf8'));
|
|
510
|
+
return typeof pkg.version === 'string' ? pkg.version.trim() : null;
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Upgrade an installed @baize-ai/baize-a2a from an uploaded .tar.gz
|
|
518
|
+
* (D49 K3 — web 重传 = 升级). Gate: incoming semver must be strictly higher
|
|
519
|
+
* than the installed one (components.json version first, skill package.json
|
|
520
|
+
* fallback); otherwise { ok:false, error:'version_not_higher', local, incoming }.
|
|
521
|
+
* Pipeline: backup skillDir/.backup/<ts> (upgrade.js .backup convention) →
|
|
522
|
+
* extract over skillDir → npm install --omit=dev + rebuild (D44) →
|
|
523
|
+
* components.json version bump → a2a CLI `restart` (covers pm2 AND nohup
|
|
524
|
+
* daemon lifecycles). Extraction/install/rebuild failure rolls the skill dir
|
|
525
|
+
* back from the backup; restart failure is reported, not rolled back (the
|
|
526
|
+
* on-disk upgrade itself succeeded).
|
|
527
|
+
*
|
|
528
|
+
* @returns {Promise<{ok:boolean, version?:string, previousVersion?:string,
|
|
529
|
+
* upgraded?:boolean, restarted?:boolean, restartError?:string, error?:string}>}
|
|
530
|
+
*/
|
|
531
|
+
export async function upgradeA2aTarball(tarballPath, {
|
|
532
|
+
skillsDir = defaultSkillsDir(),
|
|
533
|
+
componentsFile: componentsPath = componentsFile(),
|
|
534
|
+
installDeps = npmInstallDeps,
|
|
535
|
+
rebuildDeps = rebuildNativeDeps,
|
|
536
|
+
restartFn = () => runA2aCli(['restart'], { timeout: 120000 }),
|
|
537
|
+
originalName = null,
|
|
538
|
+
} = {}) {
|
|
539
|
+
const file = String(tarballPath || '');
|
|
540
|
+
const verdict = validateA2aTarball(file, originalName);
|
|
541
|
+
if (verdict.error) return { ok: false, error: verdict.error };
|
|
542
|
+
const { pkg, entries } = verdict;
|
|
543
|
+
const targetDir = path.join(skillsDir, 'a2a');
|
|
544
|
+
const components = readComponents(componentsPath);
|
|
545
|
+
if (!fs.existsSync(targetDir) || !components.a2a) {
|
|
546
|
+
return { ok: false, error: 'a2a 未安装,请先安装' };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const localVersion = components.a2a.version || readInstalledPkgVersion(targetDir);
|
|
550
|
+
const cmp = compareA2aSemver(pkg.version, localVersion);
|
|
551
|
+
if (cmp === null) {
|
|
552
|
+
return { ok: false, error: '版本号无法解析,拒绝升级', local: localVersion || null, incoming: pkg.version };
|
|
553
|
+
}
|
|
554
|
+
if (cmp <= 0) {
|
|
555
|
+
return { ok: false, error: 'version_not_higher', local: localVersion || null, incoming: pkg.version };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Backup before any write (mirrors cli/lib/upgrade.js step2_backup).
|
|
559
|
+
// Copied per top-level entry: cpSync refuses a destination that is a
|
|
560
|
+
// subdirectory of its source (.backup lives inside the skill dir).
|
|
561
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
562
|
+
const backupDir = path.join(targetDir, '.backup', timestamp);
|
|
563
|
+
try {
|
|
564
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
565
|
+
for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
|
|
566
|
+
if (['node_modules', '.backup', '.baize'].includes(entry.name)) continue;
|
|
567
|
+
fs.cpSync(path.join(targetDir, entry.name), path.join(backupDir, entry.name), { recursive: true });
|
|
568
|
+
}
|
|
569
|
+
} catch (err) {
|
|
570
|
+
return { ok: false, error: `备份失败,升级中止: ${err.message}` };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Rollback: clear everything except the backup itself, then copy the
|
|
574
|
+
// backup back (top-level .backup inside targetDir must survive the wipe).
|
|
575
|
+
const restoreBackup = async () => {
|
|
576
|
+
try {
|
|
577
|
+
for (const entry of fs.readdirSync(targetDir)) {
|
|
578
|
+
if (entry !== '.backup') fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true });
|
|
579
|
+
}
|
|
580
|
+
fs.cpSync(backupDir, targetDir, { recursive: true });
|
|
581
|
+
// Best-effort dependency restore for the rolled-back code (node_modules
|
|
582
|
+
// was excluded from the backup); failure here is reported, not fatal.
|
|
583
|
+
await installDeps(targetDir);
|
|
584
|
+
return true;
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
extractTarballTo(file, targetDir, entries);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
const restored = await restoreBackup();
|
|
594
|
+
return { ok: false, error: `解压失败(${restored ? '已回滚' : '回滚失败,请检查 .backup'}): ${err.message}` };
|
|
595
|
+
}
|
|
596
|
+
const depResult = await installDeps(targetDir);
|
|
597
|
+
if (!depResult.ok) {
|
|
598
|
+
const restored = await restoreBackup();
|
|
599
|
+
return { ok: false, error: `npm install 失败(${restored ? '已回滚' : '回滚失败,请检查 .backup'}): ${depResult.error}` };
|
|
600
|
+
}
|
|
601
|
+
const rebuildResult = await rebuildDeps(targetDir);
|
|
602
|
+
if (!rebuildResult.ok) {
|
|
603
|
+
const restored = await restoreBackup();
|
|
604
|
+
return { ok: false, error: `native 模块重建失败(${restored ? '已回滚' : '回滚失败,请检查 .backup'}): ${rebuildResult.error}` };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
components.a2a.version = pkg.version;
|
|
608
|
+
components.a2a.upgradedAt = new Date().toISOString();
|
|
609
|
+
try {
|
|
610
|
+
writeComponents(componentsPath, components);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
const restored = await restoreBackup();
|
|
613
|
+
return { ok: false, error: `components.json 更新失败(${restored ? '已回滚' : '回滚失败,请检查 .backup'}): ${err.message}` };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const restart = await restartFn();
|
|
617
|
+
return {
|
|
618
|
+
ok: true,
|
|
619
|
+
version: pkg.version,
|
|
620
|
+
previousVersion: localVersion || null,
|
|
621
|
+
upgraded: true,
|
|
622
|
+
restarted: Boolean(restart?.success),
|
|
623
|
+
...(restart?.success ? {} : { restartError: restart?.error || 'restart 未返回成功' }),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Web upload entry (D49 K3): 未安装 = 安装(D25 现状);已安装 = 走升级管线。
|
|
629
|
+
* The server route delegates here so the dispatch decision lives next to the
|
|
630
|
+
* install/upgrade implementations.
|
|
631
|
+
*/
|
|
632
|
+
export async function installOrUpgradeA2aTarball(tarballPath, opts = {}) {
|
|
633
|
+
const skillsDir = opts.skillsDir ?? defaultSkillsDir();
|
|
634
|
+
const componentsPath = opts.componentsFile ?? componentsFile();
|
|
635
|
+
const targetDir = path.join(skillsDir, 'a2a');
|
|
636
|
+
const installed = fs.existsSync(targetDir) || Boolean(readComponents(componentsPath).a2a);
|
|
637
|
+
if (installed) return upgradeA2aTarball(tarballPath, opts);
|
|
638
|
+
return installA2aTarball(tarballPath, opts);
|
|
639
|
+
}
|
|
640
|
+
|
|
459
641
|
function schedulerDbPath() {
|
|
460
642
|
return path.join(baizeDir(), 'scheduler', 'scheduler.db');
|
|
461
643
|
}
|
|
@@ -464,6 +646,23 @@ function a2aDbPath() {
|
|
|
464
646
|
return process.env.BAIZE_A2A_DB || path.join(baizeDir(), 'components', 'a2a', 'a2a.db');
|
|
465
647
|
}
|
|
466
648
|
|
|
649
|
+
/**
|
|
650
|
+
* D49: read the `baize init` A2A bootstrap result written by the detached
|
|
651
|
+
* `cli.js start --background` child (see baize-a2a daemon-ctl.bootstrapStatePath).
|
|
652
|
+
* Tolerant when the file does not exist (a2a never bootstrapped).
|
|
653
|
+
* @returns {{status?:string, via?:string, pid?:number, error?:string, at?:string}|null}
|
|
654
|
+
*/
|
|
655
|
+
export function getA2aBootstrapState() {
|
|
656
|
+
const file = process.env.BAIZE_A2A_BOOTSTRAP_STATE
|
|
657
|
+
|| path.join(baizeDir(), 'components', 'a2a', 'bootstrap-state.json');
|
|
658
|
+
try {
|
|
659
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
660
|
+
return raw && typeof raw === 'object' ? raw : null;
|
|
661
|
+
} catch {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
467
666
|
/**
|
|
468
667
|
* Task board: task_runs rows created by the A2A worker (source = 'a2a', §3).
|
|
469
668
|
* Tolerant when the table/DB does not exist yet (baize-a2a not started).
|