@baize-ai/core 0.3.0 → 0.3.1

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 CHANGED
@@ -5,6 +5,11 @@ 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.1] - 2026-08-25
9
+
10
+ ### Added
11
+ - web-console「定时任务」只读列表视图(`/api/admin/scheduler/tasks`:tasks 表列表,无管理操作;scheduler 未启动容错)
12
+
8
13
  ## [0.3.0] - 2026-08-25
9
14
 
10
15
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baize-ai/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Baize (白泽) — autonomous AI agent infrastructure",
6
6
  "main": "cli/baize.js",
@@ -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
- if (!chatView || !modelView || !channelsView || !a2aView) return;
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
+ });