@jacksontian/kite-server 0.1.0 → 0.3.0

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/web/index.html CHANGED
@@ -5,7 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
6
  <meta name="theme-color" content="#111827">
7
7
  <title>Kite 控制台</title>
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
8
+ <!-- xterm 随包自托管(vendor/ 为锁版本的官方产物):不依赖 CDN,
9
+ 杜绝 CDN 投毒/HTTP 下被篡改注入窃取密钥的供应链风险 -->
10
+ <link rel="stylesheet" href="vendor/xterm.min.css">
9
11
  <style>
10
12
  :root {
11
13
  --bg: #111827; --panel: #1f2937; --line: #374151;
@@ -29,6 +31,12 @@
29
31
  #worker-status.online { color: var(--ok); }
30
32
  #worker-status.offline { color: var(--err); }
31
33
 
34
+ /* 明文 HTTP 部署的风险提示(仅非 localhost 的 http: 环境显示) */
35
+ #http-warn {
36
+ background: rgba(239,68,68,.12); border: 1px solid var(--err); color: var(--err);
37
+ border-radius: 10px; padding: 8px 12px; font-size: 12px; line-height: 1.6; margin-bottom: 14px;
38
+ }
39
+
32
40
  .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px; margin-bottom: 14px; }
33
41
  label { display: block; font-size: 12px; color: var(--dim); margin: 10px 0 4px; }
34
42
  input, textarea, select {
@@ -137,6 +145,12 @@
137
145
  </div>
138
146
  </header>
139
147
 
148
+ <!-- 明文 HTTP 部署警告:任务内容与签名流量可被窃听,建议尽快上 HTTPS -->
149
+ <div id="http-warn" class="hidden">
150
+ ⚠️ 当前为明文 HTTP 访问:任务、日志与签名在网络链路上明文可见,且本机无法使用安全加密 API。
151
+ 请勿在不信任的网络(如公共 WiFi)使用,并尽快为服务端配置 HTTPS(如 Caddy 自动证书)。
152
+ </div>
153
+
140
154
  <!-- 鉴权设置:密钥已存 localStorage 时折叠成一行,点「修改」才展开 -->
141
155
  <div class="panel" id="auth-panel">
142
156
  <div id="auth-bar" class="hidden">
@@ -163,6 +177,9 @@
163
177
  <button data-type="chat">留言</button>
164
178
  </div>
165
179
 
180
+ <label>目标 Worker(任务在哪台机器执行)</label>
181
+ <select id="f-worker"><option value="">任意在线 Worker</option></select>
182
+
166
183
  <label>标题</label>
167
184
  <input id="f-title" placeholder="一句话描述这个任务">
168
185
 
@@ -223,11 +240,15 @@
223
240
 
224
241
  <div id="toast"></div>
225
242
 
226
- <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
227
- <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
243
+ <script src="vendor/xterm.min.js"></script>
244
+ <script src="vendor/addon-fit.min.js"></script>
228
245
  <script>
229
246
  'use strict';
230
247
  const $ = (id) => document.getElementById(id);
248
+ // 非 localhost 的明文 HTTP 环境:常驻风险提示(localhost 本地联调不打扰)
249
+ if (location.protocol === 'http:' && !['localhost', '127.0.0.1'].includes(location.hostname)) {
250
+ $('http-warn').classList.remove('hidden');
251
+ }
231
252
  const secretInput = $('secret');
232
253
  secretInput.value = localStorage.getItem('rw_secret') || '';
233
254
 
@@ -499,19 +520,75 @@ function fmtTime(ts) {
499
520
  `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
500
521
  }
501
522
 
523
+ let workerList = []; // 最近一次 Worker 快照:渲染目标下拉与 Agent 联动
524
+
525
+ /** 相对时间:离线 Worker 的最近心跳展示成「X 分钟前」,超过一天回退具体时间 */
526
+ function relTime(ts) {
527
+ if (!ts) return '从未';
528
+ const diff = Date.now() - ts;
529
+ if (diff < 60_000) return '1 分钟内';
530
+ if (diff < 3600_000) return `${Math.floor(diff / 60_000)} 分钟前`;
531
+ if (diff < 86400_000) return `${Math.floor(diff / 3600_000)} 小时前`;
532
+ return fmtTime(ts);
533
+ }
534
+
502
535
  function renderWorkers(workers) {
536
+ workerList = workers;
503
537
  const ws = $('worker-status');
504
- const online = workers.find((w) => w.online);
505
- const text = online ? `Mac Mini 在线(${online.workerId})` : 'Mac Mini 离线';
538
+ const online = workers.filter((w) => w.online).length;
539
+ let text;
540
+ if (!workers.length) {
541
+ text = '无 Worker';
542
+ } else if (online) {
543
+ text = `${online}/${workers.length} Worker 在线`;
544
+ } else {
545
+ // 全离线时给出最近一次心跳时间,便于判断是「刚掉线」还是「长期没上线」
546
+ const lastSeen = Math.max(...workers.map((w) => w.lastSeenAt || 0));
547
+ text = `Worker 全部离线(最近心跳 ${relTime(lastSeen)})`;
548
+ }
506
549
  if (ws.textContent !== text) ws.textContent = text; // 没变就不写,避免无谓重绘
507
550
  ws.className = online ? 'online' : 'offline';
508
- renderAgentOptions(workers);
551
+ renderWorkerOptions(workers);
552
+ }
553
+ // 「X 分钟前」随时间变化:存在离线 Worker 时定期用最近快照重渲染,
554
+ // 保持离线时长读数新鲜(全在线时不动下拉框,避免打扰交互)
555
+ setInterval(() => {
556
+ if (workerList.length && !workerList.some((w) => w.online)) renderWorkers(workerList);
557
+ }, 30_000);
558
+
559
+ // 目标 Worker 下拉:保持用户已选项;无有效选择时默认第一个在线 Worker
560
+ function renderWorkerOptions(workers) {
561
+ const sel = $('f-worker');
562
+ const prev = sel.value;
563
+ sel.textContent = '';
564
+ const anyOpt = document.createElement('option');
565
+ anyOpt.value = '';
566
+ anyOpt.textContent = '任意在线 Worker';
567
+ sel.appendChild(anyOpt);
568
+ for (const w of workers) {
569
+ const opt = document.createElement('option');
570
+ opt.value = w.workerId;
571
+ const name = w.hostname && w.hostname !== w.workerId ? `${w.workerId}(${w.hostname})` : w.workerId;
572
+ // 离线时附上最近心跳时间,选 Worker 时一眼看出掉线多久
573
+ opt.textContent = name + (w.online ? '' : `(离线,最近心跳 ${relTime(w.lastSeenAt)})`);
574
+ sel.appendChild(opt);
575
+ }
576
+ if ([...sel.options].some((o) => o.value === prev)) sel.value = prev; // 保持用户已选项
577
+ else {
578
+ const firstOnline = workers.find((w) => w.online);
579
+ sel.value = firstOnline ? firstOnline.workerId : '';
580
+ }
581
+ renderAgentOptions(); // 选择可能变了,同步 Agent 列表
509
582
  }
510
583
 
511
- // Agent 下拉:优先在线 Worker 的列表;都离线时用最近一次上报兜底
512
- function renderAgentOptions(workers) {
513
- const src = workers.filter((w) => w.online);
514
- const agents = [...new Set((src.length ? src : workers).flatMap((w) => w.agents || []))];
584
+ // Agent 下拉:选了具体 Worker 只列它上报的 agents;「任意」模式优先在线
585
+ // Worker 的并集,都离线时用最近一次上报兜底
586
+ function renderAgentOptions() {
587
+ const selected = workerList.find((w) => w.workerId === $('f-worker').value);
588
+ const src = selected
589
+ ? [selected]
590
+ : (() => { const on = workerList.filter((w) => w.online); return on.length ? on : workerList; })();
591
+ const agents = [...new Set(src.flatMap((w) => w.agents || []))];
515
592
  if (!agents.length) return; // Worker 尚未上报,保留默认 claude 选项
516
593
  const sel = $('f-agent');
517
594
  const prev = sel.value;
@@ -524,6 +601,7 @@ function renderAgentOptions(workers) {
524
601
  }
525
602
  if ([...sel.options].some((o) => o.value === prev)) sel.value = prev; // 保持用户已选项
526
603
  }
604
+ $('f-worker').addEventListener('change', renderAgentOptions);
527
605
 
528
606
  function createTaskEl(t) {
529
607
  const div = document.createElement('div');
@@ -592,7 +670,9 @@ function patchTask(t, entry) {
592
670
  entry.toggle.textContent = openTasks.has(t.id) ? '收起' : '展开';
593
671
  }
594
672
  entry.term.classList.toggle('hidden', t.type !== 'pty'); // pty 任务入口
595
- const meta = `${t.type} · ${fmtTime(t.createdAt)}${t.finishedAt ? ' → ' + fmtTime(t.finishedAt) : ''}`;
673
+ // Worker 标注:已领取显示执行者,pending 定向任务显示目标
674
+ const workerPart = t.workerId ? `@${t.workerId}` : (t.targetWorker ? `→ ${t.targetWorker}` : '');
675
+ const meta = `${t.type}${workerPart ? ' · ' + workerPart : ''} · ${fmtTime(t.createdAt)}${t.finishedAt ? ' → ' + fmtTime(t.finishedAt) : ''}`;
596
676
  if (entry.metaText !== meta) { entry.metaText = meta; entry.meta.textContent = meta; }
597
677
  entry.el.classList.toggle('open', openTasks.has(t.id));
598
678
  patchTerm(t, entry);
@@ -721,7 +801,7 @@ function ensureTerm(t) {
721
801
  let ent = termEntries.get(t.id);
722
802
  if (ent) return ent;
723
803
  if (!window.Terminal || !window.FitAddon) {
724
- toast('xterm.js 加载失败(依赖 CDN,请检查网络后刷新)');
804
+ toast('xterm.js 加载失败(vendor/ 资源缺失,请刷新重试)');
725
805
  return null;
726
806
  }
727
807
  const xt = new Terminal({
@@ -882,7 +962,9 @@ $('btn-submit').addEventListener('click', async () => {
882
962
  $('btn-submit').disabled = true;
883
963
  const autoTitle = payload.prompt || payload.command || payload.script ||
884
964
  (currentType === 'pty' ? '交互终端' : payload.message || '留言');
885
- await api('/api/tasks', { type: currentType, title: title || autoTitle, payload });
965
+ // 目标 Worker:空值 = 任意 Worker 均可领取(undefined 不会被序列化进 body)
966
+ const targetWorker = $('f-worker').value || undefined;
967
+ await api('/api/tasks', { type: currentType, title: title || autoTitle, payload, targetWorker });
886
968
  toast(currentType === 'pty' ? '已提交,任务运行后点「进入终端」' : '任务已提交');
887
969
  $('f-title').value = '';
888
970
  if (!wsReady) await refresh();