@jacksontian/kite-server 0.4.0 → 0.4.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/lib/summary.js CHANGED
@@ -7,6 +7,7 @@ export function taskSummary(t) {
7
7
  id: t.id,
8
8
  type: t.type,
9
9
  title: t.title,
10
+ project: (t.payload && t.payload.project) || null,
10
11
  status: t.status,
11
12
  createdAt: t.createdAt,
12
13
  startedAt: t.startedAt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacksontian/kite-server",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "kite 云端信箱 API:HMAC 签名鉴权、任务队列、WebSocket 实时推送、手机端 Web 控制台",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/web/index.html CHANGED
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
6
  <meta name="theme-color" content="#111827">
7
+ <script>
8
+ // 主题初始化(先于样式解析,避免先闪一下另一套配色):本地偏好 > 系统外观
9
+ document.documentElement.dataset.theme = localStorage.getItem('rw_theme')
10
+ || (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
11
+ </script>
7
12
  <title>Kite 控制台</title>
8
13
  <!-- xterm 随包自托管(vendor/ 为锁版本的官方产物):不依赖 CDN,
9
14
  杜绝 CDN 投毒/HTTP 下被篡改注入窃取密钥的供应链风险 -->
@@ -13,6 +18,14 @@
13
18
  --bg: #111827; --panel: #1f2937; --line: #374151;
14
19
  --text: #e5e7eb; --dim: #9ca3af; --accent: #3b82f6;
15
20
  --ok: #22c55e; --err: #ef4444; --warn: #f59e0b;
21
+ --term-bg: #0b1220; --log-text: #a5f3fc;
22
+ }
23
+ /* light 主题:xterm 是 canvas 绘制读不到 CSS 变量,终端色由 JS 同步切 */
24
+ :root[data-theme='light'] {
25
+ --bg: #f4f6f9; --panel: #fff; --line: #d9dee7;
26
+ --text: #1f2937; --dim: #64748b; --accent: #2563eb;
27
+ --ok: #16a34a; --err: #dc2626; --warn: #d97706;
28
+ --term-bg: #fff; --log-text: #0e7490;
16
29
  }
17
30
  * { box-sizing: border-box; margin: 0; padding: 0; }
18
31
  body {
@@ -30,6 +43,10 @@
30
43
  #worker-status { font-size: 12px; padding: 4px 10px; border-radius: 999px; background: var(--panel); color: var(--dim); white-space: nowrap; }
31
44
  #worker-status.online { color: var(--ok); }
32
45
  #worker-status.offline { color: var(--err); }
46
+ #btn-theme {
47
+ flex: none; font-size: 12px; padding: 4px 10px; border-radius: 8px;
48
+ background: var(--panel); color: var(--dim); border: 1px solid var(--line); white-space: nowrap;
49
+ }
33
50
 
34
51
  /* 明文 HTTP 部署的风险提示(仅非 localhost 的 http: 环境显示) */
35
52
  #http-warn {
@@ -66,10 +83,10 @@
66
83
  .b-canceled { background: rgba(156,163,175,.15); color: var(--dim); }
67
84
 
68
85
  .logs {
69
- margin-top: 8px; background: #0b1220; border-radius: 8px; padding: 8px;
86
+ margin-top: 8px; background: var(--term-bg); border-radius: 8px; padding: 8px;
70
87
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; line-height: 1.6;
71
88
  max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
72
- color: #a5f3fc; display: none;
89
+ color: var(--log-text); display: none;
73
90
  }
74
91
  .task.open .logs { display: block; }
75
92
  .task .actions { margin-top: 8px; display: flex; gap: 8px; }
@@ -82,7 +99,7 @@
82
99
 
83
100
  /* 交互终端全屏覆盖层(pty 任务) */
84
101
  #term-overlay {
85
- position: fixed; inset: 0; z-index: 50; background: #0b1220;
102
+ position: fixed; inset: 0; z-index: 50; background: var(--term-bg);
86
103
  display: flex; flex-direction: column;
87
104
  padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom);
88
105
  }
@@ -147,6 +164,7 @@
147
164
  <div class="header-right">
148
165
  <span id="conn-mode">连接中</span>
149
166
  <span id="worker-status">检测中…</span>
167
+ <button id="btn-theme" type="button" title="切换 浅色/深色 主题">浅色</button>
150
168
  </div>
151
169
  </header>
152
170
 
@@ -242,6 +260,32 @@ function toast(msg) {
242
260
  setTimeout(() => { el.style.opacity = '0'; }, 2200);
243
261
  }
244
262
 
263
+ // ---------- 主题:light/dark 切换,偏好存 localStorage,首访跟随系统外观 ----------
264
+ // xterm 是 canvas 绘制读不到 CSS 变量,终端配色在这里维护并同步给各实例;
265
+ // 浅色下重排 16 色调色板(默认 ANSI 的黄/亮黄在白底上不可见)
266
+ const TERM_THEME = {
267
+ dark: { background: '#0b1220', foreground: '#e5e7eb' },
268
+ light: {
269
+ background: '#ffffff', foreground: '#111827', cursor: '#111827',
270
+ black: '#111827', red: '#dc2626', green: '#16a34a', yellow: '#ca8a04',
271
+ blue: '#2563eb', magenta: '#9333ea', cyan: '#0891b2', white: '#f3f4f6',
272
+ brightBlack: '#6b7280', brightRed: '#ef4444', brightGreen: '#22c55e',
273
+ brightYellow: '#eab308', brightBlue: '#3b82f6', brightMagenta: '#a855f7',
274
+ brightCyan: '#22d3ee', brightWhite: '#ffffff',
275
+ },
276
+ };
277
+ function currentTheme() {
278
+ return document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
279
+ }
280
+ function applyTheme(theme) {
281
+ document.documentElement.dataset.theme = theme;
282
+ localStorage.setItem('rw_theme', theme);
283
+ $('btn-theme').textContent = theme === 'light' ? '深色' : '浅色';
284
+ document.querySelector('meta[name="theme-color"]').content = theme === 'light' ? '#f4f6f9' : '#111827';
285
+ for (const ent of termEntries.values()) ent.xt.options.theme = { ...TERM_THEME[theme] };
286
+ }
287
+ $('btn-theme').addEventListener('click', () => applyTheme(currentTheme() === 'light' ? 'dark' : 'light'));
288
+
245
289
  // ---------- 纯 JS SHA-256 / HMAC-SHA256 ----------
246
290
  // 非 HTTPS 环境(http://VPS-IP)下浏览器的 crypto.subtle 不可用,故内联实现。
247
291
  // 密钥只参与本地 HMAC 运算,从不随请求发送。
@@ -847,6 +891,15 @@ const ATTACH_HINTS = {
847
891
  };
848
892
  const KEY_SEQ = { BS: '\x7f', Tab: '\t', Esc: '\x1b', Up: '\x1b[A', Down: '\x1b[B', CtrlC: '\x03', CtrlD: '\x04' };
849
893
 
894
+ // 终端标题要有区分度:自定义标题照用;默认标题(空或「交互终端…」)
895
+ // 退化为「项目目录 · #任务短号」,同项目开多个终端也不会重名
896
+ function termLabel(t) {
897
+ if (t.title && !/^交互终端/.test(t.title)) return t.title;
898
+ const short = String(t.id).slice(-4);
899
+ const proj = t.project || (t.payload && t.payload.project);
900
+ return proj ? `${proj} · #${short}` : `终端 · #${short}`;
901
+ }
902
+
850
903
  function termStatus(ent) {
851
904
  const el = $('term-status');
852
905
  if (!ent) { el.textContent = ''; return; }
@@ -864,7 +917,7 @@ function ensureTerm(t) {
864
917
  }
865
918
  const xt = new Terminal({
866
919
  fontSize: 13, scrollback: 3000, cursorBlink: true,
867
- theme: { background: '#0b1220', foreground: '#e5e7eb' },
920
+ theme: { ...TERM_THEME[currentTheme()] },
868
921
  });
869
922
  const fit = new FitAddon.FitAddon();
870
923
  xt.loadAddon(fit);
@@ -895,7 +948,7 @@ function openTerm(t) {
895
948
  const ent = ensureTerm(t);
896
949
  if (!ent) return;
897
950
  activeTermId = t.id;
898
- $('term-title').textContent = t.title || '交互终端';
951
+ $('term-title').textContent = termLabel(t);
899
952
  $('term-overlay').classList.remove('hidden');
900
953
  showTermPane(t.id); // 先亮出本终端的 pane 再 fit(隐藏状态下 fit 拿不到尺寸)
901
954
  fitActiveTerm();
@@ -998,7 +1051,7 @@ $('btn-submit').addEventListener('click', async () => {
998
1051
  if (!/^[\w.-]+$/.test(payload.project)) return toast('项目目录名只能含字母/数字/._-');
999
1052
  try {
1000
1053
  $('btn-submit').disabled = true;
1001
- await api('/api/tasks', { type: 'pty', title: title || '交互终端', payload, targetWorker });
1054
+ await api('/api/tasks', { type: 'pty', title: title || `交互终端(${payload.project})`, payload, targetWorker });
1002
1055
  toast('已提交,任务运行后点「进入终端」');
1003
1056
  $('f-title').value = '';
1004
1057
  if (!wsReady) await refresh();
@@ -1009,6 +1062,7 @@ $('btn-submit').addEventListener('click', async () => {
1009
1062
  }
1010
1063
  });
1011
1064
 
1065
+ applyTheme(currentTheme()); // 主题初始化:同步按钮文案与 theme-color(head 已设好 data-theme)
1012
1066
  connectWS();
1013
1067
  // 首次加载兜底:WS 握手前的空档先用 REST 铺一次数据(无密钥时不发请求)
1014
1068
  if (hasSecret()) refresh();
package/web/pc.html CHANGED
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="theme-color" content="#111827">
7
+ <script>
8
+ // 主题初始化(先于样式解析,避免先闪一下另一套配色):本地偏好 > 系统外观
9
+ document.documentElement.dataset.theme = localStorage.getItem('rw_theme')
10
+ || (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
11
+ </script>
7
12
  <title>Kite 控制台</title>
8
13
  <!-- xterm 随包自托管(vendor/ 为锁版本的官方产物):不依赖 CDN,
9
14
  杜绝 CDN 投毒/HTTP 下被篡改注入窃取密钥的供应链风险 -->
@@ -13,6 +18,14 @@
13
18
  --bg: #111827; --panel: #1f2937; --line: #374151;
14
19
  --text: #e5e7eb; --dim: #9ca3af; --accent: #3b82f6;
15
20
  --ok: #22c55e; --err: #ef4444; --warn: #f59e0b;
21
+ --term-bg: #0b1220; --log-text: #a5f3fc;
22
+ }
23
+ /* light 主题:xterm 是 canvas 绘制读不到 CSS 变量,终端色由 JS 同步切 */
24
+ :root[data-theme='light'] {
25
+ --bg: #f4f6f9; --panel: #fff; --line: #d9dee7;
26
+ --text: #1f2937; --dim: #64748b; --accent: #2563eb;
27
+ --ok: #16a34a; --err: #dc2626; --warn: #d97706;
28
+ --term-bg: #fff; --log-text: #0e7490;
16
29
  }
17
30
  * { box-sizing: border-box; margin: 0; padding: 0; }
18
31
  body {
@@ -30,6 +43,11 @@
30
43
  #worker-status { font-size: 13px; padding: 4px 12px; border-radius: 999px; background: var(--panel); color: var(--dim); white-space: nowrap; }
31
44
  #worker-status.online { color: var(--ok); }
32
45
  #worker-status.offline { color: var(--err); }
46
+ #btn-fullscreen, #btn-theme {
47
+ font-size: 13px; padding: 4px 12px; border-radius: 8px; cursor: pointer;
48
+ background: var(--panel); color: var(--dim); border: 1px solid var(--line); white-space: nowrap;
49
+ }
50
+ #btn-fullscreen:hover, #btn-theme:hover { border-color: var(--accent); color: var(--accent); }
33
51
 
34
52
  /* 明文 HTTP 部署的风险提示(仅非 localhost 的 http: 环境显示) */
35
53
  #http-warn {
@@ -75,10 +93,10 @@
75
93
  .b-canceled { background: rgba(156,163,175,.15); color: var(--dim); }
76
94
 
77
95
  .logs {
78
- margin-top: 10px; background: #0b1220; border-radius: 8px; padding: 10px;
96
+ margin-top: 10px; background: var(--term-bg); border-radius: 8px; padding: 10px;
79
97
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.6;
80
98
  max-height: min(60vh, 640px); overflow-y: auto; white-space: pre-wrap; word-break: break-all;
81
- color: #a5f3fc; display: none;
99
+ color: var(--log-text); display: none;
82
100
  }
83
101
  .task.open .logs { display: block; }
84
102
  .task .actions { margin-top: 10px; display: flex; gap: 8px; }
@@ -91,7 +109,7 @@
91
109
 
92
110
  /* 交互终端全屏覆盖层(pty 任务);PC 有物理键盘,输入直接由 xterm 承接 */
93
111
  #term-overlay {
94
- position: fixed; inset: 0; z-index: 50; background: #0b1220;
112
+ position: fixed; inset: 0; z-index: 50; background: var(--term-bg);
95
113
  display: flex; flex-direction: column;
96
114
  }
97
115
  #term-overlay.hidden { display: none; }
@@ -113,7 +131,8 @@
113
131
  background: var(--panel); color: var(--dim); border: 1px solid var(--line);
114
132
  }
115
133
  #term-layout-btns button.active { border-color: var(--accent); color: var(--accent); background: rgba(59,130,246,.12); }
116
- #term-close { font-size: 13px; padding: 5px 16px; border-radius: 6px; background: var(--panel); color: var(--text); border: 1px solid var(--line); cursor: pointer; }
134
+ #term-fullscreen, #term-close { font-size: 13px; padding: 5px 16px; border-radius: 6px; background: var(--panel); color: var(--text); border: 1px solid var(--line); cursor: pointer; white-space: nowrap; }
135
+ #term-fullscreen:hover { border-color: var(--accent); color: var(--accent); }
117
136
  /* 单屏模式:pane 绝对定位叠层,只显示 active 的一个;
118
137
  并排(grid)模式:所有终端按 CSS grid 平铺,同时盯多个 pty */
119
138
  #term-body { flex: 1; margin: 0 16px 16px; min-height: 0; position: relative; }
@@ -183,6 +202,8 @@
183
202
  <div class="header-right">
184
203
  <span id="conn-mode">连接中</span>
185
204
  <span id="worker-status">检测中…</span>
205
+ <button id="btn-theme" type="button" title="切换 浅色/深色 主题">浅色</button>
206
+ <button id="btn-fullscreen" type="button">全屏</button>
186
207
  </div>
187
208
  </header>
188
209
 
@@ -253,6 +274,7 @@
253
274
  <button type="button" data-layout="single">单屏</button>
254
275
  <button type="button" data-layout="grid">并排</button>
255
276
  </div>
277
+ <button id="term-fullscreen" type="button">全屏</button>
256
278
  <button id="term-close" type="button">收起</button>
257
279
  </div>
258
280
  <div id="term-body"></div>
@@ -278,6 +300,32 @@ function toast(msg) {
278
300
  setTimeout(() => { el.style.opacity = '0'; }, 2200);
279
301
  }
280
302
 
303
+ // ---------- 主题:light/dark 切换,偏好存 localStorage,首访跟随系统外观 ----------
304
+ // xterm 是 canvas 绘制读不到 CSS 变量,终端配色在这里维护并同步给各实例;
305
+ // 浅色下重排 16 色调色板(默认 ANSI 的黄/亮黄在白底上不可见)
306
+ const TERM_THEME = {
307
+ dark: { background: '#0b1220', foreground: '#e5e7eb' },
308
+ light: {
309
+ background: '#ffffff', foreground: '#111827', cursor: '#111827',
310
+ black: '#111827', red: '#dc2626', green: '#16a34a', yellow: '#ca8a04',
311
+ blue: '#2563eb', magenta: '#9333ea', cyan: '#0891b2', white: '#f3f4f6',
312
+ brightBlack: '#6b7280', brightRed: '#ef4444', brightGreen: '#22c55e',
313
+ brightYellow: '#eab308', brightBlue: '#3b82f6', brightMagenta: '#a855f7',
314
+ brightCyan: '#22d3ee', brightWhite: '#ffffff',
315
+ },
316
+ };
317
+ function currentTheme() {
318
+ return document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
319
+ }
320
+ function applyTheme(theme) {
321
+ document.documentElement.dataset.theme = theme;
322
+ localStorage.setItem('rw_theme', theme);
323
+ $('btn-theme').textContent = theme === 'light' ? '深色' : '浅色';
324
+ document.querySelector('meta[name="theme-color"]').content = theme === 'light' ? '#f4f6f9' : '#111827';
325
+ for (const ent of termEntries.values()) ent.xt.options.theme = { ...TERM_THEME[theme] };
326
+ }
327
+ $('btn-theme').addEventListener('click', () => applyTheme(currentTheme() === 'light' ? 'dark' : 'light'));
328
+
281
329
  // ---------- 纯 JS SHA-256 / HMAC-SHA256 ----------
282
330
  // 非 HTTPS 环境(http://VPS-IP)下浏览器的 crypto.subtle 不可用,故内联实现。
283
331
  // 密钥只参与本地 HMAC 运算,从不随请求发送。
@@ -889,6 +937,15 @@ const ATTACH_HINTS = {
889
937
  unknown: '任务尚未被领取或 Worker 离线,请稍候…',
890
938
  };
891
939
 
940
+ // 终端 tab/pane 的显示名要有区分度:自定义标题照用;默认标题(空或「交互终端…」)
941
+ // 退化为「项目目录 · #任务短号」,同项目开多个终端也不会重名
942
+ function termLabel(t) {
943
+ if (t.title && !/^交互终端/.test(t.title)) return t.title;
944
+ const short = String(t.id).slice(-4);
945
+ const proj = t.project || (t.payload && t.payload.project);
946
+ return proj ? `${proj} · #${short}` : `终端 · #${short}`;
947
+ }
948
+
892
949
  // 状态条挂在各终端自己的 pane 头部(并排时各有各的状态)
893
950
  function termStatus(ent) {
894
951
  if (!ent || !ent.statusEl) return;
@@ -906,7 +963,7 @@ function ensureTerm(t) {
906
963
  }
907
964
  const xt = new Terminal({
908
965
  fontSize: 14, scrollback: 5000, cursorBlink: true,
909
- theme: { background: '#0b1220', foreground: '#e5e7eb' },
966
+ theme: { ...TERM_THEME[currentTheme()] },
910
967
  });
911
968
  const fit = new FitAddon.FitAddon();
912
969
  xt.loadAddon(fit);
@@ -920,7 +977,7 @@ function ensureTerm(t) {
920
977
  <button class="tp-close" type="button" title="关闭该终端">✕</button>
921
978
  </div>
922
979
  <div class="tp-term"></div>`;
923
- pane.querySelector('.tp-title').textContent = t.title || '交互终端'; // textContent 天然防注入
980
+ pane.querySelector('.tp-title').textContent = termLabel(t); // textContent 天然防注入
924
981
  pane.querySelector('.tp-close').addEventListener('click', (ev) => {
925
982
  ev.stopPropagation();
926
983
  disposeTerm(t.id); // 只释放本地实例,任务卡上「进入终端」可随时重进(pty.attach 回补缓冲)
@@ -934,7 +991,7 @@ function ensureTerm(t) {
934
991
  const tab = document.createElement('div');
935
992
  tab.className = 'term-tab';
936
993
  tab.innerHTML = '<span class="tt-dot"></span><span class="tt-title"></span>';
937
- tab.querySelector('.tt-title').textContent = t.title || '交互终端';
994
+ tab.querySelector('.tt-title').textContent = termLabel(t);
938
995
  tab.addEventListener('click', () => activateTerm(t.id));
939
996
  $('term-tabs').appendChild(tab);
940
997
  ent = {
@@ -1010,6 +1067,8 @@ function openTerm(t) {
1010
1067
  }
1011
1068
 
1012
1069
  function closeTermOverlay() {
1070
+ // 覆盖层自身处于全屏时先退出全屏,否则会留一个隐藏的黑屏全屏层
1071
+ if (fsElement() === $('term-overlay')) exitFs();
1013
1072
  $('term-overlay').classList.add('hidden');
1014
1073
  activeTermId = null;
1015
1074
  if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
@@ -1044,9 +1103,37 @@ document.querySelectorAll('#term-layout-btns button').forEach((btn) => {
1044
1103
  applyTermLayout();
1045
1104
  });
1046
1105
  });
1106
+ // ---------- 全屏(Fullscreen API:页头整页全屏 / 终端覆盖层单独全屏)----------
1107
+ function fsElement() { return document.fullscreenElement || document.webkitFullscreenElement; }
1108
+ function exitFs() {
1109
+ if (!fsElement()) return;
1110
+ (document.exitFullscreen || document.webkitExitFullscreen).call(document);
1111
+ }
1112
+ function toggleFs(el) {
1113
+ if (fsElement()) { exitFs(); return; }
1114
+ try {
1115
+ const p = el.requestFullscreen ? el.requestFullscreen()
1116
+ : (el.webkitRequestFullscreen ? el.webkitRequestFullscreen() : null);
1117
+ if (p && p.catch) p.catch(() => toast('进入全屏失败(浏览器限制,请重试)'));
1118
+ else if (!p) toast('当前浏览器不支持全屏');
1119
+ } catch { toast('当前浏览器不支持全屏'); }
1120
+ }
1121
+ function syncFsButtons() {
1122
+ const text = fsElement() ? '退出全屏' : '全屏';
1123
+ $('btn-fullscreen').textContent = text;
1124
+ $('term-fullscreen').textContent = text;
1125
+ }
1126
+ document.addEventListener('fullscreenchange', () => {
1127
+ syncFsButtons();
1128
+ setTimeout(fitVisibleTerms, 120); // 全屏进出后视口尺寸变了,终端重 fit
1129
+ });
1130
+ document.addEventListener('webkitfullscreenchange', syncFsButtons);
1131
+ $('btn-fullscreen').addEventListener('click', () => toggleFs(document.documentElement));
1132
+ $('term-fullscreen').addEventListener('click', () => toggleFs($('term-overlay')));
1047
1133
  // Esc 收起终端覆盖层(终端聚焦时 Esc 会先被 xterm 吃掉,此处兜底非聚焦场景)
1048
1134
  document.addEventListener('keydown', (ev) => {
1049
1135
  if (ev.key === 'Escape' && activeTermId && !$('term-overlay').classList.contains('hidden')) {
1136
+ if (fsElement()) return; // 全屏态 Esc 由浏览器用于退出全屏,不再联动收起
1050
1137
  const ent = termEntries.get(activeTermId);
1051
1138
  // 终端聚焦时 Esc 是有效按键(如退出 less),双击 Esc 才收起,避免误触
1052
1139
  if (ent && document.activeElement && document.activeElement.closest
@@ -1084,7 +1171,7 @@ $('btn-submit').addEventListener('click', async () => {
1084
1171
  if (!/^[\w.-]+$/.test(payload.project)) return toast('项目目录名只能含字母/数字/._-');
1085
1172
  try {
1086
1173
  $('btn-submit').disabled = true;
1087
- await api('/api/tasks', { type: 'pty', title: title || '交互终端', payload, targetWorker });
1174
+ await api('/api/tasks', { type: 'pty', title: title || `交互终端(${payload.project})`, payload, targetWorker });
1088
1175
  toast('已提交,任务运行后点「进入终端」');
1089
1176
  $('f-title').value = '';
1090
1177
  if (!wsReady) await refresh();
@@ -1095,6 +1182,7 @@ $('btn-submit').addEventListener('click', async () => {
1095
1182
  }
1096
1183
  });
1097
1184
 
1185
+ applyTheme(currentTheme()); // 主题初始化:同步按钮文案与 theme-color(head 已设好 data-theme)
1098
1186
  connectWS();
1099
1187
  // 首次加载兜底:WS 握手前的空档先用 REST 铺一次数据(无密钥时不发请求)
1100
1188
  if (hasSecret()) refresh();