nexus-agentd 0.2.6 → 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.
Files changed (56) hide show
  1. package/README.md +103 -14
  2. package/dist/a2a/runtime.d.ts +4 -1
  3. package/dist/a2a/runtime.js +213 -10
  4. package/dist/acp/runtime.js +13 -6
  5. package/dist/artifact-store.d.ts +168 -0
  6. package/dist/artifact-store.js +1111 -0
  7. package/dist/cli.js +13 -14
  8. package/dist/config.js +45 -5
  9. package/dist/control-plane-validation.d.ts +54 -0
  10. package/dist/control-plane-validation.js +152 -0
  11. package/dist/control-plane.d.ts +9 -32
  12. package/dist/control-plane.js +41 -157
  13. package/dist/drivers/index.js +12 -0
  14. package/dist/drivers/pi.js +1 -1
  15. package/dist/drivers/stdio.d.ts +4 -1
  16. package/dist/drivers/stdio.js +135 -19
  17. package/dist/events.d.ts +9 -0
  18. package/dist/events.js +17 -4
  19. package/dist/index.js +10 -1
  20. package/dist/process-tree.js +15 -6
  21. package/dist/run-store.d.ts +90 -7
  22. package/dist/run-store.js +254 -22
  23. package/dist/server/auth-policy.d.ts +27 -0
  24. package/dist/server/auth-policy.js +143 -0
  25. package/dist/server/connection.d.ts +8 -0
  26. package/dist/server/connection.js +67 -0
  27. package/dist/server/diagnostics.d.ts +40 -0
  28. package/dist/server/diagnostics.js +130 -0
  29. package/dist/server/metrics.d.ts +51 -0
  30. package/dist/server/metrics.js +56 -0
  31. package/dist/server/quota.d.ts +59 -0
  32. package/dist/server/quota.js +176 -0
  33. package/dist/server/validation.d.ts +29 -0
  34. package/dist/server/validation.js +221 -0
  35. package/dist/server.d.ts +3 -0
  36. package/dist/server.js +737 -419
  37. package/dist/session/admin-runs.d.ts +73 -0
  38. package/dist/session/admin-runs.js +298 -0
  39. package/dist/session/admission.d.ts +32 -0
  40. package/dist/session/admission.js +62 -0
  41. package/dist/session/readiness.d.ts +33 -0
  42. package/dist/session/readiness.js +174 -0
  43. package/dist/session.d.ts +65 -7
  44. package/dist/session.js +256 -109
  45. package/dist/sse-writer.d.ts +7 -0
  46. package/dist/sse-writer.js +60 -0
  47. package/dist/types.d.ts +46 -2
  48. package/dist/types.js +1 -0
  49. package/dist/webui/app/sources.js +18 -9
  50. package/dist/webui/icons.d.ts +1 -0
  51. package/dist/webui/icons.js +1 -0
  52. package/dist/webui/markup.js +3 -0
  53. package/dist/webui/styles.js +105 -15
  54. package/examples/client.mjs +102 -0
  55. package/nexus-agentd.example.json +19 -0
  56. package/package.json +3 -2
@@ -1,6 +1,7 @@
1
1
  export declare const icons: {
2
2
  overview: string;
3
3
  runs: string;
4
+ artifacts: string;
4
5
  agents: string;
5
6
  workspaces: string;
6
7
  keys: string;
@@ -2,6 +2,7 @@ const base = (paths) => `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="
2
2
  export const icons = {
3
3
  overview: base('<rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/>'),
4
4
  runs: base('<path d="M22 12h-2.5a2 2 0 0 0-1.93 1.46l-2.1 7.4a.25.25 0 0 1-.48 0L9.24 3.14a.25.25 0 0 0-.48 0l-2.1 7.4A2 2 0 0 1 4.73 12H2"/>'),
5
+ artifacts: base('<path d="M21 8v8a2 2 0 0 1-1 1.73l-7 4a2 2 0 0 1-2 0l-7-4A2 2 0 0 1 3 16V8a2 2 0 0 1 1-1.73l7-4a2 2 0 0 1 2 0l7 4A2 2 0 0 1 21 8Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>'),
5
6
  agents: base('<path d="M12 8V4.5"/><circle cx="12" cy="3" r="1.1"/><rect x="4" y="8" width="16" height="12" rx="3"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M9.5 13v1.6"/><path d="M14.5 13v1.6"/><path d="M9.5 17.2h5"/>'),
6
7
  workspaces: base('<path d="M20 20a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-7.1a2 2 0 0 1-1.66-.89l-.75-1.12A2 2 0 0 0 8.83 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2Z"/>'),
7
8
  keys: base('<circle cx="8" cy="15" r="4.2"/><path d="m11.1 12 8-8"/><path d="m15.4 7.7 2.9 2.9"/><path d="m17.6 5.5 2.9 2.9"/>'),
@@ -5,6 +5,7 @@ export const markup = String.raw `
5
5
  <form id="setup-form" class="auth-card">
6
6
  <header><span class="brand-mark">N</span><h1>初始化 Agent Nexus</h1></header>
7
7
  <p>此密码仅用于登录控制台;客户端 API 密钥在登录后单独创建。</p>
8
+ <div class="field"><label for="setup-token">初始化令牌</label><input id="setup-token" name="setupToken" type="password" autocomplete="off" required><small class="field-help">从网关启动终端复制 Setup token。重启后令牌会更新;远程初始化请使用可信网络或 HTTPS。</small></div>
8
9
  <div class="field"><label for="setup-password">控制台密码</label><input id="setup-password" name="password" type="password" minlength="12" autocomplete="new-password" required></div>
9
10
  <div class="field"><label for="setup-confirm">确认密码</label><input id="setup-confirm" name="confirmPassword" type="password" minlength="12" autocomplete="new-password" required></div>
10
11
  <button class="button primary auth-submit" type="submit">完成初始化</button>
@@ -29,6 +30,7 @@ export const markup = String.raw `
29
30
  <p class="nav-group-label" id="nav-group-run">运行</p>
30
31
  <button class="nav-item active" data-page="overview">${icons.overview}<span>总览</span></button>
31
32
  <button class="nav-item" data-page="runs">${icons.runs}<span>运行记录</span></button>
33
+ <button class="nav-item" data-page="artifacts">${icons.artifacts}<span>产物</span></button>
32
34
  </div>
33
35
  <div class="nav-group" role="group" aria-labelledby="nav-group-config">
34
36
  <p class="nav-group-label" id="nav-group-config">网关配置</p>
@@ -48,6 +50,7 @@ export const markup = String.raw `
48
50
  <div id="page-actions" class="page-actions"></div>
49
51
  </header>
50
52
  <section id="page-content" class="page-content">
53
+ <p id="connection-status" class="connection-status hidden" role="status"></p>
51
54
  <div id="page-stats"></div>
52
55
  <div id="page-toolbar"></div>
53
56
  <div id="page-results"></div>
@@ -39,11 +39,11 @@ export const styles = String.raw `
39
39
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
40
40
  --sp-5: 20px; --sp-6: 24px; --sp-8: 32px;
41
41
 
42
- /* 字号阶梯,全站只用这六级:
42
+ /* 字号阶梯,全站只用这六级(面向中文正文做了整体上调):
43
43
  2xs 大写微标签(表头 / badge)· xs 辅助说明 · sm 正文与表格
44
44
  md 面板与 drawer 标题 · lg 页面标题 · stat 统计数字 */
45
- --fs-2xs: 11px; --fs-xs: 12px; --fs-sm: 13px;
46
- --fs-md: 14px; --fs-lg: 16px; --fs-stat: 28px;
45
+ --fs-2xs: 12px; --fs-xs: 13px;
46
+ --fs-sm: 14px; --fs-md: 15px; --fs-lg: 17px; --fs-stat: 28px;
47
47
 
48
48
  --radius: 6px;
49
49
  --radius-lg: calc(var(--radius) + 2px);
@@ -117,7 +117,7 @@ body {
117
117
  margin: 0;
118
118
  background: hsl(var(--background));
119
119
  color: hsl(var(--foreground));
120
- font: var(--fs-sm)/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
120
+ font: var(--fs-sm)/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
121
121
  "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif;
122
122
  -webkit-font-smoothing: antialiased;
123
123
  text-rendering: optimizeLegibility;
@@ -126,7 +126,7 @@ h1, h2, h3, p { margin: 0; }
126
126
  button, input, select, textarea { font: inherit; color: inherit; }
127
127
  button { background: none; border: 0; cursor: pointer; padding: 0; }
128
128
  code, .mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
129
- .hidden { display: none !important; }
129
+ .hidden, [hidden] { display: none !important; }
130
130
  .muted { color: hsl(var(--muted-foreground)); font-size: var(--fs-xs); }
131
131
 
132
132
  :focus-visible {
@@ -220,6 +220,7 @@ input[type="checkbox"] { width: 14px; height: 14px; padding: 0; accent-color: hs
220
220
  .button.solid-danger:hover:not(:disabled) { background: hsl(var(--destructive) / .9); color: hsl(var(--destructive-foreground)); }
221
221
  .button.small { height: 26px; padding: 0 var(--sp-2); font-size: var(--fs-xs); }
222
222
  .button svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
223
+ a.button { color: hsl(var(--foreground)); text-decoration: none; }
223
224
 
224
225
  .icon-button {
225
226
  display: inline-flex; align-items: center; justify-content: center;
@@ -327,7 +328,7 @@ button[aria-busy="true"]::after { content: '…'; margin-left: var(--sp-1); }
327
328
  .empty-state .button { margin-top: var(--sp-3); }
328
329
 
329
330
  /* Stats */
330
- .stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); }
331
+ .stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-4); }
331
332
  .stat {
332
333
  padding: var(--sp-4) var(--sp-5);
333
334
  background: hsl(var(--card));
@@ -337,6 +338,8 @@ button[aria-busy="true"]::after { content: '…'; margin-left: var(--sp-1); }
337
338
  }
338
339
  .stat span { color: hsl(var(--muted-foreground)); font-size: var(--fs-xs); font-weight: 500; }
339
340
  .stat strong { font-size: var(--fs-stat); font-weight: 600; letter-spacing: -.02em; line-height: 1.2; font-variant-numeric: tabular-nums; }
341
+ a.stat { color: inherit; text-decoration: none; }
342
+ a.stat:hover { border-color: hsl(var(--muted-foreground) / .4); box-shadow: var(--shadow-sm); }
340
343
 
341
344
  /* ── Toolbar ──────────────────────────────────────────────────────── */
342
345
  .toolbar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
@@ -359,12 +362,14 @@ tbody tr:last-child td { border-bottom: 0; }
359
362
  tbody tr:hover { background: hsl(var(--muted) / .5); }
360
363
  tr[data-run-detail] { cursor: pointer; }
361
364
 
362
- .agent-name { display: grid; gap: 2px; min-width: 0; }
365
+ .agent-name { display: grid; gap: var(--sp-1); min-width: 0; }
363
366
  .agent-name strong { font-size: var(--fs-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
364
367
  .agent-name small { color: hsl(var(--muted-foreground)); font-size: var(--fs-xs); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
365
368
  .error-detail { color: hsl(var(--destructive-text)); font-size: var(--fs-sm); overflow-wrap: anywhere; line-height: 1.4; }
366
369
  .row-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); }
367
370
  .agent-table { min-width: 1050px; table-layout: fixed; }
371
+ .connection-status { padding: var(--sp-3) var(--sp-4); border: 1px solid currentColor; border-radius: var(--radius-lg); color: hsl(var(--destructive-text)); margin-bottom: var(--sp-4); overflow-wrap: anywhere; }
372
+ .run-pagination { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
368
373
  .agent-table th:nth-child(1) { width: 22%; }
369
374
  .agent-table th:nth-child(2) { width: 8%; }
370
375
  .agent-table th:nth-child(3), .agent-table th:nth-child(4), .agent-table th:nth-child(6) { width: 10%; }
@@ -375,7 +380,7 @@ tr[data-run-detail] { cursor: pointer; }
375
380
  /* ── Badges & status ──────────────────────────────────────────────── */
376
381
  .badge {
377
382
  display: inline-flex; align-items: center;
378
- height: 20px; padding: 0 var(--sp-2);
383
+ height: 22px; padding: 0 var(--sp-2);
379
384
  border: 1px solid hsl(var(--border));
380
385
  border-radius: 4px;
381
386
  background: hsl(var(--secondary));
@@ -384,7 +389,7 @@ tr[data-run-detail] { cursor: pointer; }
384
389
  }
385
390
  .status {
386
391
  display: inline-flex; align-items: center; gap: var(--sp-2);
387
- height: 22px; padding: 0 var(--sp-2);
392
+ height: 24px; padding: 0 var(--sp-2);
388
393
  border-radius: 9999px;
389
394
  background: hsl(var(--secondary));
390
395
  color: hsl(var(--muted-foreground));
@@ -431,11 +436,11 @@ tr[data-run-detail] { cursor: pointer; }
431
436
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
432
437
  overflow: hidden;
433
438
  color: hsl(var(--foreground));
434
- font-size: var(--fs-sm); line-height: 1.5;
439
+ font-size: var(--fs-sm); line-height: 1.6;
435
440
  white-space: pre-wrap;
436
441
  overflow-wrap: anywhere;
437
442
  }
438
- .run-progress { display: grid; gap: 2px; }
443
+ .run-progress { display: grid; gap: var(--sp-1); }
439
444
  .run-progress strong { font-size: var(--fs-xs); font-weight: 600; }
440
445
  .run-progress small { color: hsl(var(--muted-foreground)); font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
441
446
  .run-meta {
@@ -456,6 +461,13 @@ tr[data-run-detail] { cursor: pointer; }
456
461
  .run-task-cell small { display: block; margin-top: 2px; color: hsl(var(--muted-foreground)); font-size: var(--fs-2xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
457
462
 
458
463
  /* Run detail (drawer) */
464
+ .run-detail-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3); }
465
+ .run-output-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-2); }
466
+ .run-output-heading h3 { margin: 0; }
467
+ .run-pending { padding: var(--sp-3); margin-bottom: var(--sp-4); border: 1px solid hsl(var(--border)); border-radius: var(--radius); }
468
+ .run-pending-prompt { white-space: pre-wrap; overflow-wrap: anywhere; }
469
+ .run-pending h3 { font-size: var(--fs-md); }
470
+ .run-pending .row-actions { flex-wrap: wrap; }
459
471
  .run-detail-grid {
460
472
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
461
473
  margin-bottom: var(--sp-4);
@@ -464,6 +476,11 @@ tr[data-run-detail] { cursor: pointer; }
464
476
  overflow: hidden;
465
477
  background: hsl(var(--border));
466
478
  }
479
+ /* 紧凑指标网格(总览系统状态等):复用 run-detail-item 的视觉,密度更高 */
480
+ .run-detail-grid.compact { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 0; }
481
+ .run-detail-grid.compact .run-detail-item { padding: var(--sp-2) var(--sp-3); }
482
+ /* 嵌在 panel 里时通到边框,避免双层描边 */
483
+ .panel > .run-detail-grid.compact { border: 0; border-radius: 0; }
467
484
  .run-detail-item { padding: var(--sp-3); background: hsl(var(--card)); display: grid; gap: var(--sp-1); align-content: start; }
468
485
  .run-detail-item span { color: hsl(var(--muted-foreground)); font-size: var(--fs-2xs); font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }
469
486
  .run-detail-item strong { font-size: var(--fs-sm); font-weight: 500; overflow-wrap: anywhere; }
@@ -493,12 +510,19 @@ tr[data-run-detail] { cursor: pointer; }
493
510
  transition: border-color var(--duration-fast) var(--ease-standard);
494
511
  }
495
512
  .list-row:hover { border-color: hsl(var(--muted-foreground) / .4); }
496
- .list-row-main { display: grid; gap: 2px; min-width: 0; }
513
+ .list-row-main { display: grid; gap: var(--sp-1); min-width: 0; }
497
514
  .list-row-main > * { min-width: 0; }
498
515
  .list-row-main strong, .path { font-size: var(--fs-sm); font-weight: 600; overflow-wrap: anywhere; }
499
516
  .list-row-main small { color: hsl(var(--muted-foreground)); font-size: var(--fs-xs); overflow-wrap: anywhere; }
500
- .panel-body > .workspace-list, .panel-body > .overview-list { gap: var(--sp-2); }
501
- .panel-body .list-row { border-radius: var(--radius); }
517
+ /* panel 里嵌套的列表去掉卡片套卡片:压平成分隔线行 */
518
+ .panel-body .overview-list, .panel-body .workspace-list { gap: 0; }
519
+ .panel-body .list-row {
520
+ border: 0; border-radius: 0; background: transparent;
521
+ border-bottom: 1px solid hsl(var(--border));
522
+ padding: var(--sp-3) 0;
523
+ min-height: 0;
524
+ }
525
+ .panel-body .list-row:last-child { border-bottom: 0; }
502
526
 
503
527
  /* ── API keys ─────────────────────────────────────────────────────── */
504
528
  .key-table { min-width: 760px; width: 100%; table-layout: fixed; }
@@ -580,6 +604,35 @@ tr[data-run-detail] { cursor: pointer; }
580
604
  font-size: var(--fs-xs);
581
605
  }
582
606
 
607
+ /* ── Artifacts page ───────────────────────────────────────────────── */
608
+ .artifact-table { min-width: 760px; table-layout: fixed; }
609
+ .artifact-table th:nth-child(1) { width: 64px; }
610
+ .artifact-table th:nth-child(3) { width: 18%; }
611
+ .artifact-table th:nth-child(4), .artifact-table th:nth-child(5) { width: 120px; }
612
+ .artifact-table th:nth-child(6) { width: 180px; }
613
+ .artifact-table td { height: 56px; }
614
+ .artifact-thumb-button {
615
+ display: block; width: 44px; height: 44px;
616
+ padding: 0; border-radius: var(--radius);
617
+ overflow: hidden;
618
+ }
619
+ .artifact-thumb { display: block; width: 44px; height: 44px; object-fit: cover; border: 1px solid hsl(var(--border)); border-radius: var(--radius); background: hsl(var(--muted) / .4); }
620
+ /* 非图片产物的类型图标瓦片 */
621
+ .artifact-tile {
622
+ display: grid; place-items: center;
623
+ width: 44px; height: 44px;
624
+ border: 1px solid hsl(var(--border));
625
+ border-radius: var(--radius);
626
+ background: hsl(var(--muted) / .4);
627
+ color: hsl(var(--muted-foreground));
628
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
629
+ }
630
+ .artifact-tile:hover { background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
631
+ .artifact-tile svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
632
+ .artifact-preview-wrap { display: grid; justify-items: center; gap: var(--sp-2); margin-bottom: var(--sp-4); }
633
+ .artifact-preview { max-width: 100%; max-height: 320px; border: 1px solid hsl(var(--border)); border-radius: var(--radius); background: hsl(var(--muted) / .4); }
634
+ .artifact-preview-wrap audio { width: 100%; }
635
+
583
636
  /* ── Drawer ───────────────────────────────────────────────────────── */
584
637
  .drawer-backdrop { position: fixed; inset: 0; background: rgb(0 0 0 / .4); z-index: var(--z-backdrop); animation: fadeIn var(--duration-fade) var(--ease-out); }
585
638
  .drawer {
@@ -723,7 +776,8 @@ tr[data-run-detail] { cursor: pointer; }
723
776
  .settings-section-heading { margin-bottom: 24px; }
724
777
  .settings-account-row { grid-template-columns: 1fr; gap: var(--sp-4); }
725
778
  .search-box { max-width: none; }
726
- .stats { grid-template-columns: 1fr; }
779
+ .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
780
+ .run-detail-grid.compact { grid-template-columns: repeat(2, minmax(0, 1fr)); }
727
781
  .run-summary { grid-template-columns: repeat(2, 1fr); }
728
782
  .run-live-grid { grid-template-columns: minmax(0, 1fr); }
729
783
  .toolbar input[type="search"], .toolbar select { max-width: none; width: 100%; }
@@ -734,6 +788,42 @@ tr[data-run-detail] { cursor: pointer; }
734
788
  .toast-region { right: var(--sp-4); bottom: var(--sp-4); left: var(--sp-4); max-width: none; }
735
789
  }
736
790
 
791
+ /* Keep row actions and task context in the viewport on laptops and phones. */
792
+ @media (max-width: 1400px) {
793
+ .agent-table, .run-table { min-width: 0; }
794
+ .agent-table thead, .run-table thead {
795
+ position: absolute; width: 1px; height: 1px; padding: 0;
796
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
797
+ }
798
+ .agent-table tbody, .run-table tbody { display: grid; }
799
+ .agent-table tbody tr, .run-table tbody tr {
800
+ display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
801
+ gap: var(--sp-3) var(--sp-6); padding: var(--sp-4); border-bottom: 1px solid hsl(var(--border));
802
+ }
803
+ .agent-table td, .run-table td {
804
+ min-width: 0; max-width: none !important; padding: 0; height: auto;
805
+ border: 0; white-space: normal !important; overflow-wrap: anywhere; text-align: left !important;
806
+ }
807
+ .agent-table td::before, .run-table td::before { display: block; font-size: var(--fs-xs); color: hsl(var(--muted-foreground)); margin-bottom: var(--sp-1); }
808
+ .agent-table td:nth-child(2)::before { content: '协议'; }
809
+ .agent-table td:nth-child(3)::before { content: '驱动'; }
810
+ .agent-table td:nth-child(4)::before { content: '探测状态'; }
811
+ .agent-table td:nth-child(5)::before { content: '默认工作区'; }
812
+ .agent-table td:nth-child(6)::before { content: '权限策略'; }
813
+ .run-table td:nth-child(2)::before { content: '任务'; }
814
+ .run-table td:nth-child(3)::before { content: '结果'; }
815
+ .run-table td:nth-child(4)::before { content: '状态'; }
816
+ .run-table td:nth-child(5)::before { content: '开始时间'; }
817
+ .run-table td:nth-child(6)::before { content: '耗时'; }
818
+ .agent-table td:last-child, .run-table td:last-child, td[colspan] { grid-column: 1 / -1; }
819
+ .agent-table .row-actions { justify-content: flex-start; }
820
+ .agent-table .button.small, .run-table .button.small { min-height: 36px; }
821
+ }
822
+ @media (max-width: 560px) {
823
+ .agent-table tbody tr, .run-table tbody tr { grid-template-columns: minmax(0, 1fr); }
824
+ .run-pagination { align-items: flex-start; }
825
+ }
826
+
737
827
  /* ── Reduced motion ───────────────────────────────────────────────── */
738
828
  /* 统一压制而不是逐个列举消费者,这样以后新增的 transition 不会漏掉。
739
829
  running/waiting 的 pulse 停下后,色点和文字标签仍然表达状态。 */
@@ -0,0 +1,102 @@
1
+ import { setTimeout as delay } from 'node:timers/promises'
2
+
3
+ /** Minimal Node 20+ client. Keep API Keys out of URLs and browser storage. */
4
+ export function createGatewayClient(baseUrl, apiKey) {
5
+ const base = new URL(baseUrl)
6
+ const headers = { Authorization: `Bearer ${apiKey}` }
7
+ const route = (sessionId) => `/v1/sessions/${encodeURIComponent(sessionId)}`
8
+ async function request(path, method = 'GET', body, signal) {
9
+ const response = await fetch(new URL(path, base), {
10
+ method, headers: { ...headers, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
11
+ body: body === undefined ? undefined : JSON.stringify(body),
12
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000),
13
+ redirect: 'error'
14
+ })
15
+ if (!response.ok) {
16
+ const error = new Error(`Gateway request failed (${response.status})`)
17
+ error.status = response.status
18
+ await response.body?.cancel()
19
+ throw error
20
+ }
21
+ return response.json()
22
+ }
23
+ return {
24
+ createSession: (agentId, workspace) => request('/v1/sessions', 'POST', { agentId, workspace }),
25
+ snapshot: (id) => request(route(id)),
26
+ message: (id, message) => request(`${route(id)}/message`, 'POST', { message }),
27
+ resolve: (id, requestId, answer) => request(`${route(id)}/requests/${encodeURIComponent(requestId)}/resolve`, 'POST', answer),
28
+ cancel: (id) => request(`${route(id)}/cancel`, 'POST', {}),
29
+ close: (id) => request(route(id), 'DELETE'),
30
+ /** A reset yields a replacement snapshot; old discarded events are not replayed. */
31
+ async *events(id, { after, signal = new AbortController().signal } = {}) {
32
+ let cursor = after
33
+ let retries = 0
34
+ while (!signal.aborted) {
35
+ const connection = new AbortController()
36
+ const combined = AbortSignal.any([signal, connection.signal])
37
+ let timer = setTimeout(() => connection.abort(new Error('SSE connection timeout')), 15_000)
38
+ let reader
39
+ let reset = false
40
+ try {
41
+ const response = await fetch(new URL(`${route(id)}/events`, base), {
42
+ headers: { ...headers, ...(cursor === undefined ? {} : { 'Last-Event-ID': cursor }) },
43
+ signal: combined, redirect: 'error'
44
+ })
45
+ clearTimeout(timer)
46
+ if (!response.ok) {
47
+ const error = new Error(`SSE request failed (${response.status})`)
48
+ error.status = response.status
49
+ await response.body?.cancel()
50
+ throw error
51
+ }
52
+ reader = response.body.getReader()
53
+ const decoder = new TextDecoder()
54
+ let buffer = ''
55
+ while (!signal.aborted && !reset) {
56
+ timer = setTimeout(() => connection.abort(new Error('SSE heartbeat timeout')), 45_000)
57
+ const { value, done } = await reader.read()
58
+ clearTimeout(timer)
59
+ if (done) break
60
+ buffer += decoder.decode(value, { stream: true }).replace(/\r/g, '')
61
+ if (buffer.length > 2 * 1024 * 1024) throw new Error('SSE frame limit exceeded')
62
+ let end
63
+ while ((end = buffer.indexOf('\n\n')) >= 0) {
64
+ const frame = buffer.slice(0, end)
65
+ buffer = buffer.slice(end + 2)
66
+ const lines = frame.split('\n')
67
+ const data = lines.filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n')
68
+ if (!data) continue // Heartbeat comments carry no event.
69
+ const type = lines.find((line) => line.startsWith('event:'))?.slice(6).trim()
70
+ const eventId = lines.find((line) => line.startsWith('id:'))?.slice(3).trim()
71
+ if (type === 'reset') {
72
+ // Use our fixed same-origin Session route, never a remote recovery URL.
73
+ const snapshot = await request(route(id), 'GET', undefined, signal)
74
+ cursor = snapshot.lastEventId || '0'
75
+ yield { type: 'reset', data: snapshot, recovery: JSON.parse(data) }
76
+ reset = true
77
+ break
78
+ }
79
+ if (!eventId || !/^\d+$/.test(eventId)) continue
80
+ if (cursor !== undefined && BigInt(eventId) <= BigInt(cursor)) continue
81
+ cursor = eventId
82
+ retries = 0
83
+ yield JSON.parse(data)
84
+ }
85
+ }
86
+ } catch (error) {
87
+ if (signal.aborted) return
88
+ // A revoked Key, narrowed scope or deleted/restarted Session needs user action.
89
+ if ([401, 403, 404].includes(error.status)) throw error
90
+ } finally {
91
+ clearTimeout(timer)
92
+ connection.abort()
93
+ await reader?.cancel().catch(() => {})
94
+ }
95
+ if (!reset && !signal.aborted) {
96
+ await delay(Math.min(30_000, 500 * 2 ** Math.min(retries++, 6)) + Math.random() * 250,
97
+ undefined, { signal }).catch((error) => { if (!signal.aborted) throw error })
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
@@ -17,7 +17,26 @@
17
17
  "maxSseConnections": 128,
18
18
  "maxConnections": 256,
19
19
  "sessionTtlMs": 86400000,
20
+ "quotas": {
21
+ "maxSessionsPerKey": 16,
22
+ "maxRunningRunsPerKey": 4,
23
+ "maxSsePerKey": 8,
24
+ "maxUploadBytesPerKey": 33554432
25
+ },
26
+ "history": { "maxRuns": 1000, "retentionDays": 30, "maxBytes": 67108864 },
27
+ "artifacts": { "retentionDays": 30, "maxBytes": 536870912, "maxArtifactBytes": 12582912, "maxQueuedBytes": 67108864 },
20
28
  "agents": {
29
+ "custom": {
30
+ "protocol": "acp",
31
+ "driver": "stdio",
32
+ "name": "Custom ACP",
33
+ "enabled": false,
34
+ "command": "your-acp-agent",
35
+ "args": [],
36
+ "probeArgs": ["--version"],
37
+ "workspace": "/data/repos/project",
38
+ "permissionPolicy": "ask"
39
+ },
21
40
  "codex": {
22
41
  "protocol": "acp",
23
42
  "driver": "codex",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-agentd",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "Local Nexus Gateway daemon with an HTTP/SSE API and WebUI for allowlisted ACP and remote A2A agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "dist",
13
+ "examples",
13
14
  "nexus-agentd.example.json",
14
15
  "README.md"
15
16
  ],
@@ -34,7 +35,7 @@
34
35
  ],
35
36
  "scripts": {
36
37
  "build": "node scripts/generate-webui-sources.mjs && node scripts/clean-dist.mjs && tsc -p tsconfig.json",
37
- "typecheck": "node scripts/generate-webui-sources.mjs && tsc -p tsconfig.json --noEmit",
38
+ "typecheck": "node scripts/generate-webui-sources.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.webui.json",
38
39
  "test": "tsx --test test/*.test.ts",
39
40
  "test:webui": "node scripts/generate-webui-sources.mjs && playwright test",
40
41
  "prepack": "npm run build"