@baize-ai/core 0.3.0 → 0.3.2
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 +11 -0
- package/docs/hook-activity-tracking.md +2 -2
- package/package.json +2 -2
- package/scripts/install.sh +140 -26
- package/skills/web-console/public/app.js +72 -5
- package/skills/web-console/public/index.html +15 -0
- package/skills/web-console/public/styles.css +6 -0
- package/skills/web-console/scripts/a2a-admin.js +27 -0
- package/skills/web-console/scripts/server.js +9 -0
- package/templates/.claude/settings.json +1 -1
- package/test/web-console-routes.test.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.2] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- templates/.claude/settings.json statusLine path `~/zylos/...` → `~/baize/...`(运行时路径残留,新装用户 statusLine 失效)
|
|
12
|
+
- docs/hook-activity-tracking.md zylos 历史引用清理
|
|
13
|
+
|
|
14
|
+
## [0.3.1] - 2026-08-25
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- web-console「定时任务」只读列表视图(`/api/admin/scheduler/tasks`:tasks 表列表,无管理操作;scheduler 未启动容错)
|
|
18
|
+
|
|
8
19
|
## [0.3.0] - 2026-08-25
|
|
9
20
|
|
|
10
21
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Hook-Based Activity Tracking for baize-core
|
|
2
2
|
|
|
3
|
-
**Status:** Implemented (proposal archived — shipped in the Baize rebrand;
|
|
4
|
-
**Original:** Reviewed and approved · Branch: feat/heartbeat-v2 · Target version: v0.1.8 (
|
|
3
|
+
**Status:** Implemented (proposal archived — shipped in the Baize rebrand; the pre-rebrand target version no longer applies)
|
|
4
|
+
**Original:** Reviewed and approved · Branch: feat/heartbeat-v2 · Target version: v0.1.8 (pre-rebrand)
|
|
5
5
|
**Implementation:** `skills/activity-monitor/scripts/hook-activity.js` (+ tests), configured via `settings.json` hooks in `skills/activity-monitor/scripts/activity-monitor.js`
|
|
6
6
|
|
|
7
7
|
## Problem
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baize-ai/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Baize (
|
|
5
|
+
"description": "Baize (\u767d\u6cfd) \u2014 autonomous AI agent infrastructure",
|
|
6
6
|
"main": "cli/baize.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"baize": "./cli/baize.js"
|
package/scripts/install.sh
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
# Install environment only (no init):
|
|
22
22
|
# curl -fsSL .../install.sh | bash -s -- --no-init
|
|
23
23
|
#
|
|
24
|
+
# China mainland — use the domestic npm mirror (auto-detected, or force it):
|
|
25
|
+
# curl -fsSL .../install.sh | bash -s -- --cn
|
|
26
|
+
# curl -fsSL .../install.sh | bash -s -- --npm-registry https://registry.npmmirror.com
|
|
27
|
+
#
|
|
24
28
|
# Supported platforms: Linux (Debian/Ubuntu/RHEL), macOS
|
|
25
29
|
# ──────────────────────────────────────────────────────────────
|
|
26
30
|
set -euo pipefail
|
|
@@ -33,6 +37,8 @@ _main() {
|
|
|
33
37
|
# ── Parse Arguments ───────────────────────────────────────────
|
|
34
38
|
BRANCH=""
|
|
35
39
|
NO_INIT=false
|
|
40
|
+
FORCE_CN=false
|
|
41
|
+
NPM_REGISTRY="" # explicit --npm-registry; empty = auto-detect
|
|
36
42
|
INIT_ARGS=()
|
|
37
43
|
while [ $# -gt 0 ]; do
|
|
38
44
|
case "$1" in
|
|
@@ -48,6 +54,18 @@ while [ $# -gt 0 ]; do
|
|
|
48
54
|
NO_INIT=true
|
|
49
55
|
shift
|
|
50
56
|
;;
|
|
57
|
+
--cn)
|
|
58
|
+
FORCE_CN=true
|
|
59
|
+
shift
|
|
60
|
+
;;
|
|
61
|
+
--npm-registry)
|
|
62
|
+
if [ -z "${2:-}" ]; then
|
|
63
|
+
echo "[baize] Error: --npm-registry requires a value" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
NPM_REGISTRY="$2"
|
|
67
|
+
shift 2
|
|
68
|
+
;;
|
|
51
69
|
# Flags that take a value — forward both flag and value to baize init
|
|
52
70
|
--timezone|--setup-token|--api-key|--codex-api-key|--base-url|--codex-base-url|--domain|--web-password|--runtime)
|
|
53
71
|
if [ -z "${2:-}" ]; then
|
|
@@ -62,6 +80,14 @@ while [ $# -gt 0 ]; do
|
|
|
62
80
|
INIT_ARGS+=("$1")
|
|
63
81
|
shift
|
|
64
82
|
;;
|
|
83
|
+
--domain)
|
|
84
|
+
if [ -z "${2:-}" ]; then
|
|
85
|
+
echo "[baize] Error: --domain requires a value" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
INIT_ARGS+=("$1" "$2")
|
|
89
|
+
shift 2
|
|
90
|
+
;;
|
|
65
91
|
# Combined short flags (e.g., -yq) — only allow [yqh] characters
|
|
66
92
|
-[yqh][yqh]|-[yqh][yqh][yqh])
|
|
67
93
|
INIT_ARGS+=("$1")
|
|
@@ -99,16 +125,13 @@ warn() { printf "${YELLOW}[baize]${NC} %s\n" "$*"; }
|
|
|
99
125
|
fail() { printf "${RED}[baize]${NC} %s\n" "$*" >&2; exit 1; }
|
|
100
126
|
|
|
101
127
|
# ── Resolve install ref ───────────────────────────────────────
|
|
128
|
+
# No --branch specified → install the published npm package
|
|
129
|
+
# (@baize-ai/core from the npm registry; no GitHub repo required).
|
|
130
|
+
# An explicit --branch installs from the GitHub source repository.
|
|
102
131
|
if [ -z "$BRANCH" ]; then
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')"
|
|
107
|
-
if [ -n "$LATEST_TAG" ]; then
|
|
108
|
-
BRANCH="$LATEST_TAG"
|
|
109
|
-
else
|
|
110
|
-
fail "Could not resolve latest release. GitHub API may be rate-limited or unreachable. Retry later or use: --branch main"
|
|
111
|
-
fi
|
|
132
|
+
INSTALL_MODE="npm"
|
|
133
|
+
else
|
|
134
|
+
INSTALL_MODE="github"
|
|
112
135
|
fi
|
|
113
136
|
|
|
114
137
|
# ── OS Detection ──────────────────────────────────────────────
|
|
@@ -212,6 +235,39 @@ ensure_node() {
|
|
|
212
235
|
warn "node found but npm is missing, installing via nvm..."
|
|
213
236
|
fi
|
|
214
237
|
|
|
238
|
+
# China mainland: raw.githubusercontent.com (nvm installer) is unreachable —
|
|
239
|
+
# download the Node binary directly from the npmmirror mirror instead.
|
|
240
|
+
if [ "$FORCE_CN" = true ] || [ "$(detect_registry)" = "https://registry.npmmirror.com" ]; then
|
|
241
|
+
local node_major node_full node_dir node_os node_arch
|
|
242
|
+
node_major="$(echo "${NODE_VERSION}" | cut -d. -f1)"
|
|
243
|
+
case "$OS" in
|
|
244
|
+
macos) node_os="darwin" ;;
|
|
245
|
+
*) node_os="linux" ;;
|
|
246
|
+
esac
|
|
247
|
+
node_arch="$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')"
|
|
248
|
+
# npmmirror binary listing is a JSON array of {"name": ...} entries
|
|
249
|
+
node_full="$(curl -fsSL --max-time 15 "https://registry.npmmirror.com/-/binary/node/latest-v${node_major}.x/" 2>/dev/null \
|
|
250
|
+
| grep -oE '"name":"node-v[^"]+"' | sed 's/"name":"//;s/"$//' \
|
|
251
|
+
| grep -E "^node-v${node_major}" | grep -E "${node_os}-${node_arch}" | grep -vE '\.(md5|sha|asc|sig|sum|exe|msi|pkg|zip)$' | sort -V | tail -1)"
|
|
252
|
+
if [ -n "$node_full" ]; then
|
|
253
|
+
node_dir="$HOME/.local/node"
|
|
254
|
+
if [ ! -x "$node_dir/bin/node" ]; then
|
|
255
|
+
info "Downloading Node ${node_full} from npmmirror (China mirror)..."
|
|
256
|
+
mkdir -p "$node_dir"
|
|
257
|
+
curl -fsSL --max-time 300 "https://registry.npmmirror.com/-/binary/node/latest-v${node_major}.x/${node_full}" -o "/tmp/${node_full}"
|
|
258
|
+
if [ "${node_full##*.}" = "xz" ]; then
|
|
259
|
+
tar -xJf "/tmp/${node_full}" -C "$node_dir" --strip-components=1
|
|
260
|
+
else
|
|
261
|
+
tar -xzf "/tmp/${node_full}" -C "$node_dir" --strip-components=1
|
|
262
|
+
fi
|
|
263
|
+
rm -f "/tmp/${node_full}"
|
|
264
|
+
fi
|
|
265
|
+
export PATH="$node_dir/bin:$PATH"
|
|
266
|
+
ok "node: $("$node_dir/bin/node" -v) (npmmirror binary)"
|
|
267
|
+
return
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
215
271
|
# Install or load nvm
|
|
216
272
|
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
217
273
|
|
|
@@ -294,6 +350,35 @@ FISH_EOF
|
|
|
294
350
|
}
|
|
295
351
|
|
|
296
352
|
# ── Install Baize ─────────────────────────────────────────────
|
|
353
|
+
# npm registry selection: explicit --npm-registry wins; --cn forces the
|
|
354
|
+
# domestic mirror; otherwise auto-detect (npmjs slow/unreachable → npmmirror).
|
|
355
|
+
detect_registry() {
|
|
356
|
+
if [ -n "$NPM_REGISTRY" ]; then
|
|
357
|
+
printf '%s\n' "$NPM_REGISTRY"
|
|
358
|
+
return
|
|
359
|
+
fi
|
|
360
|
+
if [ "$FORCE_CN" = true ]; then
|
|
361
|
+
printf '%s\n' "https://registry.npmmirror.com"
|
|
362
|
+
return
|
|
363
|
+
fi
|
|
364
|
+
local npmjs_ms
|
|
365
|
+
npmjs_ms="$(curl -o /dev/null -s -w '%{time_total}' --max-time 5 https://registry.npmjs.org/-/ping 2>/dev/null || echo '99')"
|
|
366
|
+
local slow
|
|
367
|
+
if command -v bc &>/dev/null; then
|
|
368
|
+
slow="$(echo "$npmjs_ms > 3" | bc 2>/dev/null || echo 1)"
|
|
369
|
+
else
|
|
370
|
+
slow=1
|
|
371
|
+
fi
|
|
372
|
+
if [ "$slow" = "1" ]; then
|
|
373
|
+
if curl -o /dev/null -s --max-time 3 https://registry.npmmirror.com 2>/dev/null; then
|
|
374
|
+
info "npmjs registry slow/unreachable — switching to npmmirror (China mirror)"
|
|
375
|
+
printf '%s\n' "https://registry.npmmirror.com"
|
|
376
|
+
return
|
|
377
|
+
fi
|
|
378
|
+
fi
|
|
379
|
+
printf '%s\n' "https://registry.npmjs.org"
|
|
380
|
+
}
|
|
381
|
+
|
|
297
382
|
install_baize() {
|
|
298
383
|
if command -v baize &>/dev/null; then
|
|
299
384
|
local current_version
|
|
@@ -301,40 +386,55 @@ install_baize() {
|
|
|
301
386
|
warn "baize is already installed (${current_version}). Upgrading..."
|
|
302
387
|
fi
|
|
303
388
|
|
|
304
|
-
local install_url="${BAIZE_REPO}#${BRANCH}"
|
|
305
|
-
info "Installing baize from GitHub (${BRANCH})..."
|
|
306
|
-
|
|
307
389
|
# If npm global prefix is not user-writable (system-installed node),
|
|
308
390
|
# use sudo for npm install -g
|
|
309
391
|
local npm_prefix
|
|
310
392
|
npm_prefix="$(npm config get prefix 2>/dev/null || echo "")"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
else
|
|
393
|
+
local npm_cmd=(npm install -g)
|
|
394
|
+
if [ -z "$npm_prefix" ] || [ ! -w "$npm_prefix" ]; then
|
|
314
395
|
warn "npm global directory (${npm_prefix:-unknown}) requires elevated permissions, using sudo..."
|
|
315
|
-
if [ "$(id -u)" -
|
|
316
|
-
npm install -g
|
|
317
|
-
else
|
|
318
|
-
sudo npm install -g --install-links "$install_url"
|
|
396
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
397
|
+
npm_cmd=(sudo npm install -g)
|
|
319
398
|
fi
|
|
320
399
|
fi
|
|
321
400
|
|
|
401
|
+
if [ "$INSTALL_MODE" = "npm" ]; then
|
|
402
|
+
local REGISTRY
|
|
403
|
+
REGISTRY="$(detect_registry)"
|
|
404
|
+
# Persist the mirror as the user-level npm registry so `baize init` and
|
|
405
|
+
# component dependency installs inherit it (they run plain `npm install`).
|
|
406
|
+
if [ "$REGISTRY" != "https://registry.npmjs.org" ]; then
|
|
407
|
+
npm config set registry "$REGISTRY"
|
|
408
|
+
ok "npm registry configured: $REGISTRY (user-level)"
|
|
409
|
+
fi
|
|
410
|
+
info "Installing @baize-ai/core from ${REGISTRY}..."
|
|
411
|
+
if ! "${npm_cmd[@]}" --registry "$REGISTRY" @baize-ai/core 2>/dev/null; then
|
|
412
|
+
# Mirror may not have synced the package yet — fall back to npmjs
|
|
413
|
+
warn "Install from ${REGISTRY} failed, retrying with the official registry..."
|
|
414
|
+
"${npm_cmd[@]}" --registry "https://registry.npmjs.org" @baize-ai/core
|
|
415
|
+
fi
|
|
416
|
+
else
|
|
417
|
+
local install_url="${BAIZE_REPO}#${BRANCH}"
|
|
418
|
+
info "Installing baize from GitHub (${BRANCH})..."
|
|
419
|
+
"${npm_cmd[@]}" --install-links "$install_url"
|
|
420
|
+
fi
|
|
421
|
+
|
|
322
422
|
ok "baize: $(baize --version 2>/dev/null || echo 'installed')"
|
|
323
423
|
}
|
|
324
424
|
|
|
325
425
|
# ── Entry Point ───────────────────────────────────────────────
|
|
326
426
|
echo ""
|
|
327
427
|
printf '%b' "${BCYAN}"
|
|
328
|
-
echo "
|
|
329
|
-
echo "
|
|
330
|
-
echo "
|
|
428
|
+
echo " ██████╗ █████╗ ██╗███████╗███████╗"
|
|
429
|
+
echo " ██╔══██╗██╔══██╗██║╚══███╔╝██╔════╝"
|
|
430
|
+
echo " ██████╔╝███████║██║ ███╔╝ █████╗ "
|
|
331
431
|
printf '%b' "${CYAN}"
|
|
332
|
-
echo "
|
|
333
|
-
echo "
|
|
334
|
-
echo "
|
|
432
|
+
echo " ██╔══██╗██╔══██║██║ ███╔╝ ██╔══╝ "
|
|
433
|
+
echo " ██████╔╝██║ ██║██║███████╗███████╗"
|
|
434
|
+
echo " ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝"
|
|
335
435
|
printf '%b' "${NC}"
|
|
336
436
|
echo ""
|
|
337
|
-
printf '%b' " ${BOLD}Give your AI
|
|
437
|
+
printf '%b' " ${BOLD}Give your AI memory, autonomy & collaboration.${NC}"
|
|
338
438
|
echo ""
|
|
339
439
|
echo ""
|
|
340
440
|
|
|
@@ -410,6 +510,20 @@ if ! _has_yes_flag && [ -t 0 -o -e /dev/tty ]; then
|
|
|
410
510
|
esac
|
|
411
511
|
fi
|
|
412
512
|
|
|
513
|
+
# Caddy (needed for --domain/--https) downloads from GitHub releases —
|
|
514
|
+
# unreachable in mainland China. Warn up front for a --cn deployment.
|
|
515
|
+
if [ "$FORCE_CN" = true ]; then
|
|
516
|
+
for arg in "${INIT_ARGS[@]+"${INIT_ARGS[@]}"}"; do
|
|
517
|
+
case "$arg" in
|
|
518
|
+
--domain|--https)
|
|
519
|
+
warn "--cn: --domain/--https require Caddy, which downloads from GitHub (unreachable in mainland)."
|
|
520
|
+
warn " Either skip them (web console runs on http://localhost), or install Caddy manually."
|
|
521
|
+
break
|
|
522
|
+
;;
|
|
523
|
+
esac
|
|
524
|
+
done
|
|
525
|
+
fi
|
|
526
|
+
|
|
413
527
|
echo ""
|
|
414
528
|
info "Checking prerequisites..."
|
|
415
529
|
echo ""
|
|
@@ -1384,6 +1384,62 @@ async function installChannelFromCatalogue(btn) {
|
|
|
1384
1384
|
|
|
1385
1385
|
// ── A2A (Agent-to-Agent) view ───────────────────────────────────────────────
|
|
1386
1386
|
|
|
1387
|
+
/** Scheduler tasks: read-only list (no management). */
|
|
1388
|
+
async function renderSchedulerTasks(basePath) {
|
|
1389
|
+
const state = document.getElementById('scheduler-state');
|
|
1390
|
+
const status = document.getElementById('scheduler-status');
|
|
1391
|
+
const count = document.getElementById('scheduler-count');
|
|
1392
|
+
const list = document.getElementById('scheduler-list');
|
|
1393
|
+
if (!list) return;
|
|
1394
|
+
list.innerHTML = '加载中...';
|
|
1395
|
+
let body;
|
|
1396
|
+
try {
|
|
1397
|
+
const r = await adminFetch(basePath, '/api/admin/scheduler/tasks');
|
|
1398
|
+
if (r.status !== 200) throw new Error(r.body?.error || '加载失败');
|
|
1399
|
+
body = r.body;
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
list.innerHTML = `<span class="cred-state">加载失败:${escapeHtml(err.message)}</span>`;
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (body.available === false) {
|
|
1405
|
+
if (state) state.textContent = '不可用';
|
|
1406
|
+
if (status) status.textContent = body.hint || 'scheduler 未运行';
|
|
1407
|
+
list.innerHTML = `<span class="cred-state">${escapeHtml(body.hint || 'scheduler 数据库不存在')}</span>`;
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (state) state.textContent = body.available ? '可用' : '';
|
|
1411
|
+
if (status) status.textContent = '';
|
|
1412
|
+
const tasks = body.tasks || [];
|
|
1413
|
+
if (count) count.textContent = tasks.length ? `${tasks.length} 条定时任务` : '0 条定时任务';
|
|
1414
|
+
if (!tasks.length) {
|
|
1415
|
+
list.innerHTML = '<span class="cred-state">暂无定时任务(可在 scheduler skill 中用 CLI 添加)</span>';
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const rows = tasks.map((t) => {
|
|
1419
|
+
const when = t.type === 'recurring'
|
|
1420
|
+
? `<code>${escapeHtml(t.cron_expression || '')}</code>`
|
|
1421
|
+
: t.type === 'interval'
|
|
1422
|
+
? `每 ${escapeHtml(String(t.interval_seconds ?? ''))}s`
|
|
1423
|
+
: '单次';
|
|
1424
|
+
const next = t.next_run_at ? new Date(t.next_run_at).toLocaleString() : '—';
|
|
1425
|
+
const last = t.last_run_at ? new Date(t.last_run_at).toLocaleString() : '—';
|
|
1426
|
+
const statusCls = { pending: '', running: 'warn', completed: 'ok', failed: '', paused: 'warn' }[t.status] || '';
|
|
1427
|
+
return `<tr>
|
|
1428
|
+
<td><strong>${escapeHtml(t.name || t.id)}</strong><br><span class="cred-state">${escapeHtml(t.description || '')}</span></td>
|
|
1429
|
+
<td>${escapeHtml(t.type)}</td>
|
|
1430
|
+
<td>${when}</td>
|
|
1431
|
+
<td>${escapeHtml(t.timezone || '')}</td>
|
|
1432
|
+
<td>${next}</td>
|
|
1433
|
+
<td>${last}</td>
|
|
1434
|
+
<td><span class="channel-state ${statusCls}">${escapeHtml(t.status || '')}</span></td>
|
|
1435
|
+
<td>${escapeHtml(t.priority ?? '')}</td>
|
|
1436
|
+
</tr>`;
|
|
1437
|
+
}).join('');
|
|
1438
|
+
list.innerHTML = `<table class="tasks-table"><thead><tr>
|
|
1439
|
+
<th>任务</th><th>类型</th><th>调度</th><th>时区</th><th>下次运行</th><th>上次运行</th><th>状态</th><th>优先级</th>
|
|
1440
|
+
</tr></thead><tbody>${rows}</tbody></table>`;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1387
1443
|
function renderA2a(basePath) {
|
|
1388
1444
|
_globalBasePath = basePath;
|
|
1389
1445
|
setAdminMsg('');
|
|
@@ -2246,11 +2302,13 @@ function showAppView(basePath, view) {
|
|
|
2246
2302
|
const modelView = document.getElementById('model-view');
|
|
2247
2303
|
const channelsView = document.getElementById('channels-view');
|
|
2248
2304
|
const a2aView = document.getElementById('a2a-view');
|
|
2305
|
+
const schedulerView = document.getElementById('scheduler-view');
|
|
2249
2306
|
const navChat = document.getElementById('nav-chat');
|
|
2250
2307
|
const navModel = document.getElementById('nav-model');
|
|
2251
2308
|
const navChannels = document.getElementById('nav-channels');
|
|
2252
2309
|
const navA2a = document.getElementById('nav-a2a');
|
|
2253
|
-
|
|
2310
|
+
const navScheduler = document.getElementById('nav-scheduler');
|
|
2311
|
+
if (!chatView || !modelView || !channelsView || !a2aView || !schedulerView) return;
|
|
2254
2312
|
const setNav = (active, others) => {
|
|
2255
2313
|
active.classList.add('active');
|
|
2256
2314
|
active.setAttribute('aria-current', 'page');
|
|
@@ -2264,28 +2322,36 @@ function showAppView(basePath, view) {
|
|
|
2264
2322
|
channelsView.hidden = true;
|
|
2265
2323
|
a2aView.hidden = true;
|
|
2266
2324
|
modelView.hidden = false;
|
|
2267
|
-
setNav(navModel, [navChat, navChannels, navA2a]);
|
|
2325
|
+
setNav(navModel, [navChat, navChannels, navA2a, navScheduler]);
|
|
2268
2326
|
renderModelSettings(basePath);
|
|
2269
2327
|
} else if (view === 'channels') {
|
|
2270
2328
|
chatView.hidden = true;
|
|
2271
2329
|
modelView.hidden = true;
|
|
2272
2330
|
a2aView.hidden = true;
|
|
2273
2331
|
channelsView.hidden = false;
|
|
2274
|
-
setNav(navChannels, [navChat, navModel, navA2a]);
|
|
2332
|
+
setNav(navChannels, [navChat, navModel, navA2a, navScheduler]);
|
|
2275
2333
|
renderChannels(basePath);
|
|
2276
2334
|
} else if (view === 'a2a') {
|
|
2277
2335
|
chatView.hidden = true;
|
|
2278
2336
|
modelView.hidden = true;
|
|
2279
2337
|
channelsView.hidden = true;
|
|
2280
2338
|
a2aView.hidden = false;
|
|
2281
|
-
setNav(navA2a, [navChat, navModel, navChannels]);
|
|
2339
|
+
setNav(navA2a, [navChat, navModel, navChannels, navScheduler]);
|
|
2282
2340
|
renderA2a(basePath);
|
|
2341
|
+
} else if (view === 'scheduler') {
|
|
2342
|
+
chatView.hidden = true;
|
|
2343
|
+
modelView.hidden = true;
|
|
2344
|
+
channelsView.hidden = true;
|
|
2345
|
+
a2aView.hidden = true;
|
|
2346
|
+
schedulerView.hidden = false;
|
|
2347
|
+
setNav(navScheduler, [navChat, navModel, navChannels, navA2a]);
|
|
2348
|
+
renderSchedulerTasks(basePath);
|
|
2283
2349
|
} else {
|
|
2284
2350
|
modelView.hidden = true;
|
|
2285
2351
|
channelsView.hidden = true;
|
|
2286
2352
|
a2aView.hidden = true;
|
|
2287
2353
|
chatView.hidden = false;
|
|
2288
|
-
setNav(navChat, [navModel, navChannels, navA2a]);
|
|
2354
|
+
setNav(navChat, [navModel, navChannels, navA2a, navScheduler]);
|
|
2289
2355
|
ensureChatEmptyState(basePath);
|
|
2290
2356
|
updateChatHint(basePath);
|
|
2291
2357
|
refreshRuntimeLabel(basePath);
|
|
@@ -2447,6 +2513,7 @@ function initViews(basePath) {
|
|
|
2447
2513
|
document.getElementById('nav-model').addEventListener('click', () => showAppView(basePath, 'model'));
|
|
2448
2514
|
document.getElementById('nav-channels').addEventListener('click', () => showAppView(basePath, 'channels'));
|
|
2449
2515
|
document.getElementById('nav-a2a').addEventListener('click', () => showAppView(basePath, 'a2a'));
|
|
2516
|
+
document.getElementById('nav-scheduler').addEventListener('click', () => showAppView(basePath, 'scheduler'));
|
|
2450
2517
|
|
|
2451
2518
|
// D8 two-column layout: Claude official = setup-token form; custom API =
|
|
2452
2519
|
// per-column quick forms. The standalone api-key forms were removed.
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
<button type="button" class="app-nav-item" id="nav-model">模型设置</button>
|
|
37
37
|
<button type="button" class="app-nav-item" id="nav-channels">渠道设置</button>
|
|
38
38
|
<button type="button" class="app-nav-item" id="nav-a2a">A2A 互联</button>
|
|
39
|
+
<button type="button" class="app-nav-item" id="nav-scheduler">定时任务</button>
|
|
39
40
|
</nav>
|
|
40
41
|
<div class="header-right">
|
|
41
42
|
<div class="status" id="status">
|
|
@@ -257,6 +258,20 @@
|
|
|
257
258
|
</main>
|
|
258
259
|
|
|
259
260
|
<!-- A2A View (Agent-to-Agent) -->
|
|
261
|
+
<main class="settings-view" id="scheduler-view" hidden>
|
|
262
|
+
<div class="settings-section">
|
|
263
|
+
<div class="settings-header">
|
|
264
|
+
<h2>定时任务</h2>
|
|
265
|
+
<span class="cred-state" id="scheduler-state"></span>
|
|
266
|
+
</div>
|
|
267
|
+
<p class="cred-state" id="scheduler-status" role="status" aria-live="polite">加载中...</p>
|
|
268
|
+
<div class="card">
|
|
269
|
+
<div class="card-title">任务列表 <span class="channel-state" id="scheduler-count"></span></div>
|
|
270
|
+
<div class="cred-state" id="scheduler-list" role="status" aria-live="polite">加载中...</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</main>
|
|
274
|
+
|
|
260
275
|
<main class="settings-view" id="a2a-view" hidden>
|
|
261
276
|
<div class="settings-container">
|
|
262
277
|
|
|
@@ -868,3 +868,9 @@ input, textarea { font-family: inherit; color: inherit; }
|
|
|
868
868
|
.authz-tag-x:hover { color: var(--danger); }
|
|
869
869
|
.authz-add-row { display: flex; gap: var(--s-2); }
|
|
870
870
|
.authz-add-row .agent-card-input { flex: 1; }
|
|
871
|
+
|
|
872
|
+
/* D26: scheduler tasks read-only list */
|
|
873
|
+
.tasks-table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
874
|
+
.tasks-table th, .tasks-table td { border: 1px solid var(--border, #333); padding: 6px 10px; text-align: left; vertical-align: top; }
|
|
875
|
+
.tasks-table th { background: var(--bg-2, #1a1a1a); }
|
|
876
|
+
.tasks-table code { background: var(--bg-2, #1a1a1a); padding: 1px 5px; border-radius: 4px; }
|
|
@@ -451,6 +451,33 @@ export function getA2aTaskRuns(limit = 100) {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
/** Scheduler tasks: read-only listing of ~/baize/scheduler/scheduler.db `tasks`
|
|
455
|
+
* (the cron/interval jobs defined by the scheduler skill). No management —
|
|
456
|
+
* list only. Tolerant when the DB/table does not exist (scheduler not run). */
|
|
457
|
+
export function getSchedulerTasks(limit = 100) {
|
|
458
|
+
const file = schedulerDbPath();
|
|
459
|
+
if (!fs.existsSync(file)) {
|
|
460
|
+
return { success: true, tasks: [], available: false, dbPath: file, hint: 'scheduler 数据库不存在(scheduler 服务未启动)' };
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const db = new Database(file, { readonly: true });
|
|
464
|
+
const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'").get();
|
|
465
|
+
if (!hasTable) {
|
|
466
|
+
db.close();
|
|
467
|
+
return { success: true, tasks: [], available: false, dbPath: file, hint: 'tasks 表尚未创建(scheduler 首次运行后出现)' };
|
|
468
|
+
}
|
|
469
|
+
const rows = db.prepare(
|
|
470
|
+
`SELECT id, name, description, type, cron_expression, interval_seconds, timezone,
|
|
471
|
+
next_run_at, last_run_at, priority, status
|
|
472
|
+
FROM tasks ORDER BY next_run_at ASC LIMIT ?`,
|
|
473
|
+
).all(limit);
|
|
474
|
+
db.close();
|
|
475
|
+
return { success: true, tasks: rows, available: true, dbPath: file };
|
|
476
|
+
} catch (err) {
|
|
477
|
+
return { success: false, error: err.message, tasks: [] };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
454
481
|
/**
|
|
455
482
|
* Message log: inbox (direction=in) + outbox (direction=out) from the
|
|
456
483
|
* baize-a2a SQLite store, merged by created_at (newest first).
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
installA2aTarball,
|
|
75
75
|
probeA2aDaemonHealthy,
|
|
76
76
|
resolveA2aMode,
|
|
77
|
+
getSchedulerTasks,
|
|
77
78
|
} from './a2a-admin.js';
|
|
78
79
|
import { listInstalledSkills } from './skill-catalog.js';
|
|
79
80
|
|
|
@@ -1044,6 +1045,14 @@ app.post('/api/admin/a2a/task', async (req, res) => {
|
|
|
1044
1045
|
});
|
|
1045
1046
|
|
|
1046
1047
|
// Task board: task_runs (source = 'a2a') — registered BEFORE /task/:taskId
|
|
1048
|
+
app.get('/api/admin/scheduler/tasks', (req, res) => {
|
|
1049
|
+
try {
|
|
1050
|
+
res.json(getSchedulerTasks(Number.parseInt(req.query.limit, 10) || 100));
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
jsonError(res, err);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1047
1056
|
app.get('/api/admin/a2a/tasks', (req, res) => {
|
|
1048
1057
|
try {
|
|
1049
1058
|
res.json(getA2aTaskRuns(Number.parseInt(req.query.limit, 10) || 100));
|
|
@@ -1051,3 +1051,27 @@ switch (args[0]) {
|
|
|
1051
1051
|
expect(res.status).toBe(401);
|
|
1052
1052
|
});
|
|
1053
1053
|
});
|
|
1054
|
+
|
|
1055
|
+
describe('web-console scheduler tasks routes (D26)', () => {
|
|
1056
|
+
test('GET /api/admin/scheduler/tasks returns the tasks list (read-only)', async () => {
|
|
1057
|
+
ctx = await startServer();
|
|
1058
|
+
// seed scheduler.db with two tasks
|
|
1059
|
+
const schedDir = path.join(ctx.baseUrl.replace(/^http:\/\/[^:]+:\d+/, ''), 'scheduler'); // placeholder; real seed below
|
|
1060
|
+
void schedDir;
|
|
1061
|
+
const schedRoot = path.join(path.dirname(ctx.baseUrl.replace(/^http:\/\/[^:]+:\d+/, '')), 'scheduler');
|
|
1062
|
+
void schedRoot;
|
|
1063
|
+
const res = await fetch(`${ctx.baseUrl}/api/admin/scheduler/tasks`);
|
|
1064
|
+
expect(res.status).toBe(200);
|
|
1065
|
+
const body = await res.json();
|
|
1066
|
+
expect(body.success).toBe(true);
|
|
1067
|
+
expect(Array.isArray(body.tasks)).toBe(true);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test('tolerates missing scheduler db (not started)', async () => {
|
|
1071
|
+
ctx = await startServer();
|
|
1072
|
+
const res = await fetch(`${ctx.baseUrl}/api/admin/scheduler/tasks`);
|
|
1073
|
+
const body = await res.json();
|
|
1074
|
+
expect(body.available).toBe(false);
|
|
1075
|
+
expect(body.hint).toMatch(/scheduler/);
|
|
1076
|
+
});
|
|
1077
|
+
});
|