llm-api-gateway-cli 1.0.4 → 1.0.6
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.en.md +346 -0
- package/README.md +104 -2
- package/cli-agent.js +186 -49
- package/cli-anthropic.js +57 -13
- package/cli-claude-code.js +59 -12
- package/cli-openai.js +48 -11
- package/lib/commands.js +397 -70
- package/lib/config.js +22 -2
- package/lib/configcmd.js +146 -45
- package/lib/hub.js +435 -43
- package/lib/i18n.js +80 -0
- package/lib/launcher.js +78 -24
- package/lib/mcp.js +16 -6
- package/lib/mcpadmin.js +331 -0
- package/lib/setup.js +101 -36
- package/package.json +2 -1
- package/public/manual.html +107 -3
- package/public/mcp.css +155 -0
- package/public/mcp.html +135 -0
- package/public/mcp.js +327 -0
- package/public/sessions.css +198 -0
- package/public/sessions.html +89 -0
- package/public/sessions.js +281 -0
- package/public/shell.html +8 -0
- package/public/shell.js +20 -4
- package/public/task.css +5 -0
- package/public/task.html +14 -1
|
@@ -0,0 +1,89 @@
|
|
|
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="/sessions.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
|
+
// 被 Tab 外壳嵌着时挂上 data-embed:styles.css 据此收起本页自己的顶栏(顶栏归外壳)
|
|
23
|
+
try {
|
|
24
|
+
if (new URLSearchParams(location.search).get('embed') === '1') {
|
|
25
|
+
document.documentElement.setAttribute('data-embed', '1');
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
/* 地址解析不了就当作没嵌入,页面照常独立使用 */
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
</script>
|
|
32
|
+
<!-- 外部集成:地址上带 ?bg=<底色> 时按底色推导整套配色。必须是阻塞脚本(排在 theme.js 之前),
|
|
33
|
+
否则会先按默认配色画一帧再变色 —— 这就是所谓的闪一下 -->
|
|
34
|
+
<script src="/tint.js"></script>
|
|
35
|
+
<script src="/theme.js" defer></script>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="app">
|
|
39
|
+
<aside class="sidebar" id="sidebar">
|
|
40
|
+
<div class="sidebar-head">
|
|
41
|
+
<a class="btn ghost block" href="/task" title="回到任务页(模型真读写你选的目录)">← 回到任务页</a>
|
|
42
|
+
<div class="cs-filter">
|
|
43
|
+
<input id="cs-search" type="search" spellcheck="false" autocomplete="off"
|
|
44
|
+
placeholder="搜索工作目录 / 模型" aria-label="搜索命令行会话" />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="session-list" id="cs-list"></div>
|
|
48
|
+
<div class="sidebar-foot">
|
|
49
|
+
<span id="cs-count">会话 —</span>
|
|
50
|
+
<span class="foot-sep">·</span>
|
|
51
|
+
<span id="cs-retention">滑动保留 —</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="store-line" id="cs-dir" title="">正在读取…</div>
|
|
54
|
+
</aside>
|
|
55
|
+
|
|
56
|
+
<main class="main">
|
|
57
|
+
<header class="topbar">
|
|
58
|
+
<div class="brand">
|
|
59
|
+
<span class="dot" id="cs-dot"></span>
|
|
60
|
+
<h1>命令行会话</h1>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="topbar-actions">
|
|
63
|
+
<a class="btn ghost" href="/manual"
|
|
64
|
+
title="操作手册:两套存储的边界、怎么在终端接着聊">手册</a>
|
|
65
|
+
<div class="theme-switch" role="group" aria-label="主题">
|
|
66
|
+
<button class="theme-btn" type="button" data-theme-option="system" title="跟随系统" aria-label="跟随系统">◐</button>
|
|
67
|
+
<button class="theme-btn" type="button" data-theme-option="light" title="白天" aria-label="白天">☀</button>
|
|
68
|
+
<button class="theme-btn" type="button" data-theme-option="dark" title="黑夜" aria-label="黑夜">☾</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
<p class="cs-banner" id="cs-banner" hidden></p>
|
|
74
|
+
|
|
75
|
+
<section class="messages" id="cs-transcript">
|
|
76
|
+
<div class="empty" id="cs-empty">
|
|
77
|
+
<h2>左边选一条会话</h2>
|
|
78
|
+
<p>
|
|
79
|
+
这里回放的是终端 <code>gateway-agent -i</code> 的对话存档(<b>只读</b>,不能在这里续写)。
|
|
80
|
+
要接着聊,选中会话后按页面上给出的 <code>--resume</code> 命令在终端继续。
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
</main>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<script src="/sessions.js"></script>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令行会话(/sessions)只读页
|
|
3
|
+
*
|
|
4
|
+
* 这一页存在的理由:终端 `gateway-agent -i` 的对话落在 `<数据根>/sessions/`,网页侧从来不读它,
|
|
5
|
+
* 用户会以为「记录丢了」。这里把它读出来给人看 —— **只读**:
|
|
6
|
+
* · 只有 GET(`/api/cli-sessions`、`/api/cli-sessions/<id>`),没有任何写入路径;
|
|
7
|
+
* · 页面里没有输入框、没有发送按钮,「接着聊」只能回终端跑 `--resume`。
|
|
8
|
+
*
|
|
9
|
+
* 纪律与 manual.js 一致:一律 `textContent` —— 会话正文(含第三方工具返回的内容)是数据,
|
|
10
|
+
* 不该有机会变成 HTML。
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
var ROLE = { system: '系统提示词', user: '你(终端)', assistant: '助手', tool: '工具结果' };
|
|
16
|
+
var CLAMP = 1500; // 单条正文超过这么多字符就先折叠,点「展开全部」再看
|
|
17
|
+
|
|
18
|
+
var dom = {
|
|
19
|
+
list: document.getElementById('cs-list'),
|
|
20
|
+
count: document.getElementById('cs-count'),
|
|
21
|
+
retention: document.getElementById('cs-retention'),
|
|
22
|
+
dir: document.getElementById('cs-dir'),
|
|
23
|
+
dot: document.getElementById('cs-dot'),
|
|
24
|
+
banner: document.getElementById('cs-banner'),
|
|
25
|
+
transcript: document.getElementById('cs-transcript'),
|
|
26
|
+
empty: document.getElementById('cs-empty'),
|
|
27
|
+
search: document.getElementById('cs-search'),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var sessions = [];
|
|
31
|
+
var current = null; // 当前打开的会话 id(列表里高亮它)
|
|
32
|
+
var keyword = '';
|
|
33
|
+
|
|
34
|
+
function el(tag, cls, content) {
|
|
35
|
+
var node = document.createElement(tag);
|
|
36
|
+
if (cls) node.className = cls;
|
|
37
|
+
if (content != null) node.textContent = String(content);
|
|
38
|
+
return node;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fmtTime(ms) {
|
|
42
|
+
if (!ms) return '—';
|
|
43
|
+
try {
|
|
44
|
+
return new Date(ms).toLocaleString('zh-CN', { hour12: false });
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return String(ms);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fmtNum(n) {
|
|
51
|
+
return Number(n || 0).toLocaleString('zh-CN');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function banner(msg, danger) {
|
|
55
|
+
if (!dom.banner) return;
|
|
56
|
+
dom.banner.textContent = msg || '';
|
|
57
|
+
dom.banner.hidden = !msg;
|
|
58
|
+
dom.banner.className = danger ? 'cs-banner danger' : 'cs-banner';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getJson(url) {
|
|
62
|
+
return fetch(url).then(function (resp) {
|
|
63
|
+
return resp.json().catch(function () {
|
|
64
|
+
return {};
|
|
65
|
+
}).then(function (data) {
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
var err = new Error(data && data.error ? data.error : 'HTTP ' + resp.status);
|
|
68
|
+
err.status = resp.status;
|
|
69
|
+
err.payload = data;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ---------- 左侧列表 ---------- */
|
|
78
|
+
|
|
79
|
+
function visible() {
|
|
80
|
+
if (!keyword) return sessions;
|
|
81
|
+
var k = keyword.toLowerCase();
|
|
82
|
+
return sessions.filter(function (s) {
|
|
83
|
+
return (s.workingDir + ' ' + s.model + ' ' + s.id).toLowerCase().indexOf(k) >= 0;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderList() {
|
|
88
|
+
var rows = visible();
|
|
89
|
+
dom.list.textContent = '';
|
|
90
|
+
if (!rows.length) {
|
|
91
|
+
dom.list.appendChild(el('p', 'store-line', sessions.length ? '没有匹配的会话' : '还没有命令行会话'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
rows.forEach(function (s) {
|
|
95
|
+
var btn = el('button', 'cs-list-item');
|
|
96
|
+
btn.type = 'button';
|
|
97
|
+
if (s.id === current) btn.className += ' active';
|
|
98
|
+
var title = el('span', 'cs-item-title', s.workingDir || '(未记录工作目录)');
|
|
99
|
+
title.title = s.workingDir || '';
|
|
100
|
+
btn.appendChild(title);
|
|
101
|
+
var meta = el('span', 'cs-item-meta', fmtTime(s.touchedAt) + ' · ' + (s.model || '—') + ' · ' + s.messageCount + ' 条');
|
|
102
|
+
btn.appendChild(meta);
|
|
103
|
+
if (s.userTurns === 0) btn.appendChild(el('span', 'cs-item-tag', '空会话'));
|
|
104
|
+
if (s.pending) btn.appendChild(el('span', 'cs-item-tag', '有待批准'));
|
|
105
|
+
btn.addEventListener('click', function () {
|
|
106
|
+
open(s.id);
|
|
107
|
+
});
|
|
108
|
+
dom.list.appendChild(btn);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ---------- 右侧回放 ---------- */
|
|
113
|
+
|
|
114
|
+
function longBlock(node, content) {
|
|
115
|
+
node.textContent = content;
|
|
116
|
+
if (content.length <= CLAMP) return node;
|
|
117
|
+
node.className += ' clamped';
|
|
118
|
+
var open = false;
|
|
119
|
+
var toggle = el('button', 'cs-toggle', '展开全部(' + fmtNum(content.length) + ' 字)');
|
|
120
|
+
toggle.type = 'button';
|
|
121
|
+
toggle.addEventListener('click', function () {
|
|
122
|
+
open = !open;
|
|
123
|
+
node.className = open ? node.className.replace(' clamped', '') : node.className + ' clamped';
|
|
124
|
+
toggle.textContent = open ? '收起' : '展开全部(' + fmtNum(content.length) + ' 字)';
|
|
125
|
+
});
|
|
126
|
+
// 折叠态:正文块 + 头部里的开关由调用方放进 .cs-msg-head
|
|
127
|
+
node.__toggle = toggle;
|
|
128
|
+
return node;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderMessage(m) {
|
|
132
|
+
var box = el('div', 'cs-msg');
|
|
133
|
+
var head = el('div', 'cs-msg-head');
|
|
134
|
+
head.appendChild(el('span', 'cs-role', ROLE[m.role] || m.role || '未知'));
|
|
135
|
+
if (m.role === 'tool' && m.tool_call_id) head.appendChild(el('span', null, '· ' + m.tool_call_id));
|
|
136
|
+
box.appendChild(head);
|
|
137
|
+
|
|
138
|
+
var content = typeof m.content === 'string' ? m.content : '';
|
|
139
|
+
if (content) {
|
|
140
|
+
var body = el('div', 'cs-content');
|
|
141
|
+
longBlock(body, content);
|
|
142
|
+
box.appendChild(body);
|
|
143
|
+
if (body.__toggle) head.appendChild(body.__toggle);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof m.reasoning_content === 'string' && m.reasoning_content) {
|
|
147
|
+
var det = el('details', 'cs-tool');
|
|
148
|
+
det.appendChild(el('summary', null, '思考过程(' + fmtNum(m.reasoning_content.length) + ' 字)'));
|
|
149
|
+
det.appendChild(el('div', null, m.reasoning_content));
|
|
150
|
+
box.appendChild(det);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var calls = Array.isArray(m.tool_calls) ? m.tool_calls : [];
|
|
154
|
+
calls.forEach(function (call) {
|
|
155
|
+
var fn = (call && call.function) || {};
|
|
156
|
+
var block = el('div', 'cs-tool');
|
|
157
|
+
block.appendChild(el('b', null, (fn.name || '未知工具') + ' '));
|
|
158
|
+
var args = typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments || {});
|
|
159
|
+
try {
|
|
160
|
+
args = JSON.stringify(JSON.parse(args), null, 2);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
/* 参数不是 JSON 就原样显示 */
|
|
163
|
+
}
|
|
164
|
+
block.appendChild(el('span', null, args));
|
|
165
|
+
box.appendChild(block);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return box;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderDoc(s) {
|
|
172
|
+
var doc = el('div', 'cs-doc');
|
|
173
|
+
var head = el('div', 'cs-head');
|
|
174
|
+
head.appendChild(el('h2', null, s.id));
|
|
175
|
+
var rows = [
|
|
176
|
+
['工作目录', s.workingDir || '—'],
|
|
177
|
+
['模型 / 模式', (s.model || '—') + ' · ' + (s.mode || '—')],
|
|
178
|
+
['消息', s.messageCount + ' 条(其中你的输入 ' + s.userTurns + ' 轮)'],
|
|
179
|
+
['用量', 'prompt ' + fmtNum(s.usage && s.usage.prompt) + ' · completion ' + fmtNum(s.usage && s.usage.completion)],
|
|
180
|
+
['最后活动', fmtTime(s.touchedAt)],
|
|
181
|
+
];
|
|
182
|
+
rows.forEach(function (pair) {
|
|
183
|
+
var row = el('div', 'cs-head-row');
|
|
184
|
+
row.appendChild(el('span', null, pair[0] + ':'));
|
|
185
|
+
row.appendChild(el('code', null, pair[1]));
|
|
186
|
+
head.appendChild(row);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
var cmdRow = el('div', 'cs-head-row');
|
|
190
|
+
cmdRow.appendChild(el('span', null, '在终端接着聊:'));
|
|
191
|
+
var cmd = el('span', 'cs-cmd');
|
|
192
|
+
cmd.appendChild(el('code', null, s.resumeCommand));
|
|
193
|
+
var copy = el('button', 'cs-toggle', '复制');
|
|
194
|
+
copy.type = 'button';
|
|
195
|
+
copy.addEventListener('click', function () {
|
|
196
|
+
var done = function (okFlag) {
|
|
197
|
+
copy.textContent = okFlag ? '已复制' : '复制失败,请手动选中';
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
201
|
+
navigator.clipboard.writeText(s.resumeCommand).then(function () {
|
|
202
|
+
done(true);
|
|
203
|
+
}, function () {
|
|
204
|
+
done(false);
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
done(false);
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
done(false);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
cmd.appendChild(copy);
|
|
214
|
+
cmdRow.appendChild(cmd);
|
|
215
|
+
head.appendChild(cmdRow);
|
|
216
|
+
|
|
217
|
+
if (s.pending) {
|
|
218
|
+
var warn = el('div', 'cs-head-row');
|
|
219
|
+
warn.appendChild(el('b', null, '注意:这条会话里有一次「待批准的写入」。'));
|
|
220
|
+
warn.appendChild(el('span', null,
|
|
221
|
+
'它是终端里的挂起态,网页侧不会替你批准 —— 回终端跑上面那条命令继续处理。'));
|
|
222
|
+
head.appendChild(warn);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
doc.appendChild(head);
|
|
226
|
+
|
|
227
|
+
if (!s.messages.length) {
|
|
228
|
+
doc.appendChild(el('p', 'cs-head-row', '这条会话还没有消息(可能只是启动了一下就退出)。'));
|
|
229
|
+
}
|
|
230
|
+
(s.messages || []).forEach(function (m) {
|
|
231
|
+
doc.appendChild(renderMessage(m || {}));
|
|
232
|
+
});
|
|
233
|
+
return doc;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function open(id) {
|
|
237
|
+
banner('');
|
|
238
|
+
return getJson('/api/cli-sessions/' + encodeURIComponent(id)).then(function (data) {
|
|
239
|
+
current = id;
|
|
240
|
+
renderList();
|
|
241
|
+
if (dom.empty) dom.empty.hidden = true;
|
|
242
|
+
dom.transcript.textContent = '';
|
|
243
|
+
dom.transcript.appendChild(renderDoc(data.session));
|
|
244
|
+
dom.transcript.scrollTop = 0;
|
|
245
|
+
}).catch(function (e) {
|
|
246
|
+
banner('读不到这条会话:' + e.message, true);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ---------- 启动 ---------- */
|
|
251
|
+
|
|
252
|
+
function loadList() {
|
|
253
|
+
return getJson('/api/cli-sessions').then(function (data) {
|
|
254
|
+
sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
|
255
|
+
dom.count.textContent = '会话 ' + sessions.length + ' / ' + (data.maxSessions || '—');
|
|
256
|
+
dom.retention.textContent = '滑动保留 ' + (data.ttlDays || '—') + ' 天';
|
|
257
|
+
dom.dir.textContent = data.dir || '—';
|
|
258
|
+
dom.dir.title = data.dir || '';
|
|
259
|
+
dom.dot.className = 'dot ok';
|
|
260
|
+
renderList();
|
|
261
|
+
// 地址里带 #<id> 就直接打开那一条(方便把某条会话的链接发给别人)
|
|
262
|
+
var hash = (location.hash || '').replace(/^#/, '');
|
|
263
|
+
if (hash) open(decodeURIComponent(hash));
|
|
264
|
+
}).catch(function (e) {
|
|
265
|
+
// 页面脚本自己的错也走这里(比如漏声明变量)—— 不吞掉,打到控制台,方便一眼定位
|
|
266
|
+
if (console && console.error) console.error('[sessions] 读列表失败', e);
|
|
267
|
+
dom.dot.className = 'dot err';
|
|
268
|
+
dom.count.textContent = '会话 —';
|
|
269
|
+
dom.dir.textContent = e.status === 403 ? '接口已禁用(服务未绑定在本机地址)' : '读不到会话目录';
|
|
270
|
+
banner('读不到命令行会话:' + e.message +
|
|
271
|
+
'(存档在 <数据根>/sessions/,由终端 gateway-agent -i 写入;这一页只读)', true);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
dom.search.addEventListener('input', function () {
|
|
276
|
+
keyword = dom.search.value.trim();
|
|
277
|
+
renderList();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
loadList();
|
|
281
|
+
})();
|
package/public/shell.html
CHANGED
|
@@ -50,6 +50,14 @@
|
|
|
50
50
|
aria-selected="false" title="操作手册:流程 + 三端指令对照(Alt+3)">
|
|
51
51
|
<span class="tab-icon" aria-hidden="true">📖</span><span class="tab-text">手册</span>
|
|
52
52
|
</button>
|
|
53
|
+
<button class="tab" type="button" role="tab" data-tab="sessions" id="tab-sessions"
|
|
54
|
+
aria-selected="false" title="命令行会话(只读):终端 gateway-agent -i 的对话存档(Alt+4)">
|
|
55
|
+
<span class="tab-icon" aria-hidden="true">⌨</span><span class="tab-text">命令行</span>
|
|
56
|
+
</button>
|
|
57
|
+
<button class="tab" type="button" role="tab" data-tab="mcp" id="tab-mcp"
|
|
58
|
+
aria-selected="false" title="MCP 工具:看/改你这把密钥的绑定、登记自己的 MCP 服务器(Alt+5)">
|
|
59
|
+
<span class="tab-icon" aria-hidden="true">🔌</span><span class="tab-text">MCP</span>
|
|
60
|
+
</button>
|
|
53
61
|
<span class="tab-ink" id="tab-ink" aria-hidden="true"></span>
|
|
54
62
|
</nav>
|
|
55
63
|
|
package/public/shell.js
CHANGED
|
@@ -69,6 +69,22 @@
|
|
|
69
69
|
hint: '操作手册与三端指令对照',
|
|
70
70
|
actions: [],
|
|
71
71
|
},
|
|
72
|
+
// 这两个是「另一套存储 / 另一件事」的入口,但**同样是外壳里的一个 Tab**:
|
|
73
|
+
// 之前把它们做成顶栏链接(还带 target=_blank)是错的 —— 用户要的是"不离开这个页面、不开新标签"。
|
|
74
|
+
{
|
|
75
|
+
id: 'sessions',
|
|
76
|
+
label: '命令行',
|
|
77
|
+
src: '/sessions',
|
|
78
|
+
hint: '终端 gateway-agent -i 的会话存档(只读)',
|
|
79
|
+
actions: [],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'mcp',
|
|
83
|
+
label: 'MCP',
|
|
84
|
+
src: '/mcp',
|
|
85
|
+
hint: '你这把密钥的 MCP:看绑定、改绑、登记自己的服务器',
|
|
86
|
+
actions: [],
|
|
87
|
+
},
|
|
72
88
|
];
|
|
73
89
|
|
|
74
90
|
/** 面板地址上带的嵌入标记:面板据此收起自己的顶栏 */
|
|
@@ -374,13 +390,13 @@ if (typeof document !== 'undefined') {
|
|
|
374
390
|
window.addEventListener('hashchange', () => openTab(S.tabFromHash(window.location.hash)));
|
|
375
391
|
window.addEventListener('resize', moveInk);
|
|
376
392
|
|
|
377
|
-
// Alt+1
|
|
393
|
+
// Alt+1 … Alt+5 直接切面板(与浏览器自身的快捷键不冲突);按 TABS 的长度自动扩展
|
|
378
394
|
document.addEventListener('keydown', (e) => {
|
|
379
395
|
if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
396
|
+
const n = Number(e.key);
|
|
397
|
+
if (!Number.isInteger(n) || n < 1 || n > S.TABS.length) return;
|
|
382
398
|
e.preventDefault();
|
|
383
|
-
openTab(S.TABS[
|
|
399
|
+
openTab(S.TABS[n - 1].id);
|
|
384
400
|
});
|
|
385
401
|
|
|
386
402
|
dom.reload.addEventListener('click', () => {
|
package/public/task.css
CHANGED
package/public/task.html
CHANGED
|
@@ -47,6 +47,19 @@
|
|
|
47
47
|
placeholder="搜索任务标题 / 目录" aria-label="搜索任务" />
|
|
48
48
|
<button class="btn ghost tiny" id="btn-task-search-clear" type="button" hidden title="清空搜索">✕</button>
|
|
49
49
|
</div>
|
|
50
|
+
<!-- 命令行会话的只读存档。顶栏那个入口在 Tab 外壳里会被收起(html[data-embed] .topbar 隐藏),
|
|
51
|
+
所以侧栏里也放一个 —— 这样无论单独打开还是嵌在外壳里都找得到。 -->
|
|
52
|
+
<!-- 命令行会话 / MCP:这两件事在外壳里已经是**两个 Tab**(顶栏「命令行」「MCP」),
|
|
53
|
+
所以嵌入时用 CSS 收起来(见 task.css 的 html[data-embed] 规则);
|
|
54
|
+
单独打开任务页时它们是这边的入口,**同标签页打开**(不再 target=_blank)。 -->
|
|
55
|
+
<a class="btn ghost tiny block" id="btn-open-sessions-side" href="/sessions"
|
|
56
|
+
title="命令行会话(只读):终端 gateway-agent -i 聊过的对话,网页任务列表里列的不是它">
|
|
57
|
+
命令行会话(只读)
|
|
58
|
+
</a>
|
|
59
|
+
<a class="btn ghost tiny block" id="btn-open-mcp-side" href="/mcp"
|
|
60
|
+
title="MCP 工具:看/改你这把密钥的绑定、登记自己的 MCP 服务器(密钥留在服务端)">
|
|
61
|
+
MCP 工具
|
|
62
|
+
</a>
|
|
50
63
|
</div>
|
|
51
64
|
<div class="session-list" id="task-list"></div>
|
|
52
65
|
<div class="sidebar-foot">
|
|
@@ -75,7 +88,7 @@
|
|
|
75
88
|
<span>聊天</span>
|
|
76
89
|
<span class="ext-mark" aria-hidden="true">↗</span>
|
|
77
90
|
</a>
|
|
78
|
-
<a class="btn ghost" id="btn-open-manual" href="/manual"
|
|
91
|
+
<a class="btn ghost" id="btn-open-manual" href="/manual"
|
|
79
92
|
title="操作手册:任务页操作流程 + 三端斜杠指令对照(输入 / 也能弹指令面板)">
|
|
80
93
|
手册
|
|
81
94
|
</a>
|