llm-api-gateway-cli 1.0.2 → 1.0.4
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 +36 -16
- package/lib/config.js +18 -5
- package/lib/hub.js +26 -11
- package/lib/launcher.js +1 -1
- package/package.json +1 -1
- package/public/app.js +18 -12
- package/public/index.html +15 -6
- package/public/manual.html +42 -7
- package/public/shell-bridge.js +100 -0
- package/public/shell.css +242 -0
- package/public/shell.html +73 -0
- package/public/shell.js +412 -0
- package/public/styles.css +81 -15
- package/public/task.css +70 -0
- package/public/task.html +19 -1
- package/public/task.js +217 -13
- package/public/theme.js +20 -0
- package/public/tint.js +5 -3
package/public/shell.css
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* Tab 外壳(/):聊天 / 任务 / 手册 在同一个页面里切换
|
|
2
|
+
*
|
|
3
|
+
* 只做「外壳」这一层的排版:配色、按钮、主题全部复用 styles.css 的变量与类
|
|
4
|
+
* (--bg / --border / --text / .btn / .dot / .theme-switch …),
|
|
5
|
+
* 所以白天 / 黑夜 / 外部底色(?bg=)三种情况下都与三个面板协调。
|
|
6
|
+
* 新加的变量(--accent-2 / --accent-2-soft)同时写在 styles.css 的深浅两套里,tests/theme.test.mjs 盯着。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
.shell-body {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
height: 100%;
|
|
13
|
+
overflow: hidden; /* 滚动条交给面板内部,外壳自己不滚 */
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ---------- 顶栏(唯一一条) ---------- */
|
|
17
|
+
|
|
18
|
+
.shell-bar {
|
|
19
|
+
flex: none;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 12px;
|
|
23
|
+
padding: 8px 14px;
|
|
24
|
+
background: var(--bg-soft);
|
|
25
|
+
border-bottom: 1px solid var(--border);
|
|
26
|
+
box-shadow: var(--shadow-xs);
|
|
27
|
+
position: relative;
|
|
28
|
+
z-index: 3;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* 顶栏底部一条极细的强调线:让「这一条是外壳、下面是面板」一眼可分 */
|
|
32
|
+
.shell-bar::after {
|
|
33
|
+
content: "";
|
|
34
|
+
position: absolute;
|
|
35
|
+
left: 0;
|
|
36
|
+
right: 0;
|
|
37
|
+
bottom: -1px;
|
|
38
|
+
height: 1px;
|
|
39
|
+
background: linear-gradient(90deg, transparent, var(--accent-2), transparent);
|
|
40
|
+
opacity: .45;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.shell-brand { flex: none; }
|
|
45
|
+
.shell-brand h1 { font-size: 14.5px; }
|
|
46
|
+
|
|
47
|
+
/* ---------- Tab 组:药丸 + 滑动高亮块 ---------- */
|
|
48
|
+
|
|
49
|
+
.tabs {
|
|
50
|
+
position: relative;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 2px;
|
|
54
|
+
padding: 3px;
|
|
55
|
+
border: 1px solid var(--border);
|
|
56
|
+
border-radius: 999px;
|
|
57
|
+
background: var(--bg-code);
|
|
58
|
+
flex: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.tab-ink {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 3px;
|
|
64
|
+
bottom: 3px;
|
|
65
|
+
left: 0;
|
|
66
|
+
width: 0;
|
|
67
|
+
border-radius: 999px;
|
|
68
|
+
background: var(--bg-raised);
|
|
69
|
+
border: 1px solid var(--border-strong);
|
|
70
|
+
box-shadow: var(--shadow-xs);
|
|
71
|
+
transition: transform var(--t), width var(--t);
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.tab {
|
|
76
|
+
position: relative;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 6px;
|
|
81
|
+
border: 1px solid transparent;
|
|
82
|
+
background: none;
|
|
83
|
+
color: var(--text-dim);
|
|
84
|
+
font: inherit;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
padding: 5px 14px;
|
|
87
|
+
border-radius: 999px;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition: color var(--t), background-color var(--t);
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tab:hover { color: var(--text); background: var(--accent-2-soft); }
|
|
94
|
+
.tab:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
95
|
+
.tab[aria-selected="true"] { color: var(--text); font-weight: 600; }
|
|
96
|
+
|
|
97
|
+
.tab-icon { font-size: 12.5px; line-height: 1; }
|
|
98
|
+
|
|
99
|
+
/* 后台有任务在跑时的脉冲点:切到别的 Tab 也知道任务没停 */
|
|
100
|
+
.tab-dot {
|
|
101
|
+
display: none;
|
|
102
|
+
width: 6px;
|
|
103
|
+
height: 6px;
|
|
104
|
+
border-radius: 50%;
|
|
105
|
+
background: var(--warn);
|
|
106
|
+
box-shadow: 0 0 0 3px var(--warn-ring);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.tab.busy .tab-dot {
|
|
110
|
+
display: block;
|
|
111
|
+
animation: tab-pulse 1.4s ease-in-out infinite;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@keyframes tab-pulse {
|
|
115
|
+
0%, 100% { opacity: .35; transform: scale(.85); }
|
|
116
|
+
50% { opacity: 1; transform: scale(1); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ---------- 当前面板的操作按钮(外壳代点面板里的按钮) ---------- */
|
|
120
|
+
|
|
121
|
+
.shell-actions {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 8px;
|
|
125
|
+
margin-left: auto;
|
|
126
|
+
min-width: 0;
|
|
127
|
+
overflow: hidden; /* 顶栏不允许被长内容顶出屏幕;工作目录那一条自己会省略号截断 */
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.shell-actions:empty { display: none; }
|
|
131
|
+
|
|
132
|
+
.shell-actions .btn.active {
|
|
133
|
+
color: var(--accent);
|
|
134
|
+
border-color: var(--border-strong);
|
|
135
|
+
background: var(--accent-soft);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* 工作目录:内容来自任务面板(桥消息 workdir),点击转发给面板里的 #workdir-chip */
|
|
139
|
+
.shell-chip {
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 8px;
|
|
143
|
+
max-width: 320px;
|
|
144
|
+
border: 1px solid var(--border);
|
|
145
|
+
background: var(--bg-code);
|
|
146
|
+
color: var(--text-dim);
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
padding: 4px 12px;
|
|
149
|
+
font: inherit;
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
transition: border-color var(--t), color var(--t);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.shell-chip:hover { border-color: var(--border-strong); color: var(--text); }
|
|
156
|
+
.shell-chip .wd-label { color: var(--text-faint); font-size: 11px; flex: none; }
|
|
157
|
+
.shell-chip code {
|
|
158
|
+
font-family: var(--mono);
|
|
159
|
+
font-size: 11.5px;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
overflow: hidden;
|
|
162
|
+
text-overflow: ellipsis;
|
|
163
|
+
/* flex 项的 min-width 默认是 auto:不写这行,长路径会把顶栏顶出屏幕(省略号也不生效) */
|
|
164
|
+
min-width: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ---------- 面板区 ---------- */
|
|
168
|
+
|
|
169
|
+
.panes {
|
|
170
|
+
position: relative;
|
|
171
|
+
flex: 1;
|
|
172
|
+
min-height: 0;
|
|
173
|
+
background: var(--bg);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.pane {
|
|
177
|
+
position: absolute;
|
|
178
|
+
inset: 0;
|
|
179
|
+
/* 切回来时淡入一下(display:none → 显示会让动画重新开始,正是想要的效果) */
|
|
180
|
+
animation: pane-in 180ms cubic-bezier(.4, 0, .2, 1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.pane[hidden] { display: none; }
|
|
184
|
+
|
|
185
|
+
.pane-frame {
|
|
186
|
+
width: 100%;
|
|
187
|
+
height: 100%;
|
|
188
|
+
border: 0;
|
|
189
|
+
display: block;
|
|
190
|
+
background: var(--bg);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@keyframes pane-in {
|
|
194
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
195
|
+
to { opacity: 1; transform: none; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* 首次激活前的骨架:懒加载时给一句「正在加载」而不是白屏。
|
|
199
|
+
z-index 是必须的 —— iframe 在后面创建,不抬起来会被它盖住(等于没骨架)。
|
|
200
|
+
[hidden] 这条也是必须的:UA 的 `[hidden]{display:none}` 会被上面那条 `display:flex` 压掉,
|
|
201
|
+
不加这一条骨架在 iframe 加载完之后仍然盖在上面(表现就是「外壳出来了、面板一直空着」)。 */
|
|
202
|
+
.pane-skeleton {
|
|
203
|
+
position: absolute;
|
|
204
|
+
inset: 0;
|
|
205
|
+
z-index: 1;
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
gap: 12px;
|
|
211
|
+
color: var(--text-faint);
|
|
212
|
+
font-size: 13px;
|
|
213
|
+
background: var(--bg);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.pane-skeleton[hidden] { display: none; }
|
|
217
|
+
|
|
218
|
+
.pane-skeleton .spinner {
|
|
219
|
+
width: 22px;
|
|
220
|
+
height: 22px;
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
border: 2px solid var(--border);
|
|
223
|
+
border-top-color: var(--accent);
|
|
224
|
+
animation: spin .8s linear infinite;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
228
|
+
|
|
229
|
+
/* ---------- 窄屏 ---------- */
|
|
230
|
+
|
|
231
|
+
@media (max-width: 860px) {
|
|
232
|
+
.shell-bar { gap: 8px; padding: 8px 10px; }
|
|
233
|
+
.shell-chip { max-width: 180px; }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@media (max-width: 700px) {
|
|
237
|
+
.tab-text { display: none; }
|
|
238
|
+
.tab { padding: 5px 12px; }
|
|
239
|
+
.shell-brand h1, .shell-brand .tag { display: none; }
|
|
240
|
+
.shell-chip { max-width: 120px; }
|
|
241
|
+
.shell-actions .btn .btn-text { display: none; }
|
|
242
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>LLM API Gateway</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
8
|
+
<link rel="stylesheet" href="/shell.css" />
|
|
9
|
+
<script>
|
|
10
|
+
// 防闪白:必须在首次绘制前把主题定下来,所以内联在这里(逻辑与 /theme.js 一致,四个页面都一样)
|
|
11
|
+
(() => {
|
|
12
|
+
try {
|
|
13
|
+
let t = localStorage.getItem('lgw.theme');
|
|
14
|
+
if (t !== 'dark' && t !== 'light') {
|
|
15
|
+
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
16
|
+
}
|
|
17
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
18
|
+
document.documentElement.style.colorScheme = t;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
</script>
|
|
24
|
+
<!-- 外部集成:地址上带 ?bg=<底色> 时按底色推导整套配色,并透传给三个面板(见 shell.js 的 paneSrc)。
|
|
25
|
+
必须是阻塞脚本(排在 theme.js 之前),否则会先按默认配色画一帧再变色 -->
|
|
26
|
+
<script src="/tint.js"></script>
|
|
27
|
+
<script src="/theme.js" defer></script>
|
|
28
|
+
</head>
|
|
29
|
+
<body class="shell-body">
|
|
30
|
+
<!-- 唯一顶栏:品牌 + Tab + 当前面板操作 + 主题。
|
|
31
|
+
三个面板自己的顶栏在嵌入时收起(html[data-embed] .topbar),避免两条横栏 -->
|
|
32
|
+
<header class="shell-bar" id="shell-bar">
|
|
33
|
+
<div class="brand shell-brand">
|
|
34
|
+
<span class="dot" id="shell-dot"></span>
|
|
35
|
+
<h1>LLM API Gateway</h1>
|
|
36
|
+
<span class="tag" id="shell-tag">Web UI</span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<nav class="tabs" id="shell-tabs" role="tablist" aria-label="页面">
|
|
40
|
+
<button class="tab" type="button" role="tab" data-tab="chat" id="tab-chat"
|
|
41
|
+
aria-selected="true" title="纯对话:不碰文件系统(Alt+1)">
|
|
42
|
+
<span class="tab-icon" aria-hidden="true">💬</span><span class="tab-text">聊天</span>
|
|
43
|
+
</button>
|
|
44
|
+
<button class="tab" type="button" role="tab" data-tab="task" id="tab-task"
|
|
45
|
+
aria-selected="false" title="任务模式:选一个目录,模型真读写(Alt+2)">
|
|
46
|
+
<span class="tab-icon" aria-hidden="true">🗂</span><span class="tab-text">任务</span>
|
|
47
|
+
<span class="tab-dot" aria-hidden="true"></span>
|
|
48
|
+
</button>
|
|
49
|
+
<button class="tab" type="button" role="tab" data-tab="manual" id="tab-manual"
|
|
50
|
+
aria-selected="false" title="操作手册:流程 + 三端指令对照(Alt+3)">
|
|
51
|
+
<span class="tab-icon" aria-hidden="true">📖</span><span class="tab-text">手册</span>
|
|
52
|
+
</button>
|
|
53
|
+
<span class="tab-ink" id="tab-ink" aria-hidden="true"></span>
|
|
54
|
+
</nav>
|
|
55
|
+
|
|
56
|
+
<div class="shell-actions" id="shell-actions"></div>
|
|
57
|
+
|
|
58
|
+
<div class="theme-switch" role="group" aria-label="主题">
|
|
59
|
+
<button class="theme-btn" type="button" data-theme-option="system" title="跟随系统" aria-label="跟随系统">◐</button>
|
|
60
|
+
<button class="theme-btn" type="button" data-theme-option="light" title="白天" aria-label="白天">☀</button>
|
|
61
|
+
<button class="theme-btn" type="button" data-theme-option="dark" title="黑夜" aria-label="黑夜">☾</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<button class="btn ghost icon-only" id="btn-reload-pane" type="button"
|
|
65
|
+
title="重新加载当前面板(面板里的状态会重置)">⟳</button>
|
|
66
|
+
</header>
|
|
67
|
+
|
|
68
|
+
<!-- 面板区:iframe 首次激活时才创建(懒加载),切走的保留在 DOM 里 —— 状态不丢 -->
|
|
69
|
+
<main class="panes" id="panes"></main>
|
|
70
|
+
|
|
71
|
+
<script src="/shell.js"></script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|