@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/README.md +69 -0
- package/config.example.yaml +18 -2
- package/config.js +340 -5
- package/deploy/kiteserver.service +24 -0
- package/package.json +5 -1
- package/server.js +227 -45
- package/web/index.html +95 -13
- package/web/pc.html +878 -0
- package/web/vendor/addon-fit.min.js +8 -0
- package/web/vendor/xterm.min.css +8 -0
- package/web/vendor/xterm.min.js +8 -0
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
|
-
|
|
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="
|
|
227
|
-
<script src="
|
|
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.
|
|
505
|
-
|
|
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
|
-
|
|
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
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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();
|