@xcanwin/manyoyo 3.8.7 → 3.9.3

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.
@@ -0,0 +1,315 @@
1
+ :root {
2
+ --bg: #f4f7f5;
3
+ --panel: #ffffff;
4
+ --panel-soft: #f0f5f2;
5
+ --line: #dbe4de;
6
+ --text: #0f2f20;
7
+ --muted: #4a6256;
8
+ --accent: #0f9d58;
9
+ --accent-strong: #087f45;
10
+ --user-bubble: #e4f5eb;
11
+ --assistant-bubble: #f7faf8;
12
+ --system-bubble: #eef4ff;
13
+ --sidebar-width: 280px;
14
+ --header-height: 70px;
15
+ --composer-height: 176px;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ margin: 0;
24
+ height: 100vh;
25
+ overflow: hidden;
26
+ font-family: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
27
+ color: var(--text);
28
+ background: radial-gradient(circle at 0 0, #d8efe2 0%, var(--bg) 45%, #f5f8f6 100%);
29
+ }
30
+
31
+ .app {
32
+ height: 100vh;
33
+ }
34
+
35
+ .sidebar {
36
+ position: fixed;
37
+ left: 0;
38
+ top: 0;
39
+ bottom: 0;
40
+ width: var(--sidebar-width);
41
+ z-index: 30;
42
+ border-right: 1px solid var(--line);
43
+ background: linear-gradient(180deg, #f9fcfa 0%, #eef6f1 100%);
44
+ padding: 16px;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 12px;
48
+ }
49
+
50
+ .brand {
51
+ font-weight: 700;
52
+ letter-spacing: 0.5px;
53
+ font-size: 16px;
54
+ }
55
+
56
+ .new-session {
57
+ display: flex;
58
+ gap: 8px;
59
+ }
60
+
61
+ .new-session input {
62
+ flex: 1;
63
+ border: 1px solid var(--line);
64
+ border-radius: 10px;
65
+ padding: 9px 10px;
66
+ background: var(--panel);
67
+ }
68
+
69
+ button {
70
+ border: none;
71
+ border-radius: 10px;
72
+ padding: 9px 12px;
73
+ font-weight: 600;
74
+ cursor: pointer;
75
+ background: var(--accent);
76
+ color: #fff;
77
+ }
78
+
79
+ button:hover {
80
+ background: var(--accent-strong);
81
+ }
82
+
83
+ button.secondary {
84
+ background: var(--panel-soft);
85
+ color: var(--text);
86
+ border: 1px solid var(--line);
87
+ }
88
+
89
+ button.secondary:hover {
90
+ background: #e6efe9;
91
+ }
92
+
93
+ #sessionList {
94
+ flex: 1;
95
+ min-height: 0;
96
+ overflow-y: auto;
97
+ padding-right: 4px;
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 8px;
101
+ }
102
+
103
+ .session-item {
104
+ text-align: left;
105
+ width: 100%;
106
+ background: var(--panel);
107
+ color: var(--text);
108
+ border: 1px solid var(--line);
109
+ padding: 10px;
110
+ border-radius: 12px;
111
+ transition: 120ms ease;
112
+ }
113
+
114
+ .session-item.active {
115
+ border-color: var(--accent);
116
+ box-shadow: 0 0 0 2px rgba(15, 157, 88, 0.15);
117
+ }
118
+
119
+ .session-name {
120
+ font-size: 13px;
121
+ font-weight: 700;
122
+ margin-bottom: 3px;
123
+ word-break: break-all;
124
+ }
125
+
126
+ .session-meta {
127
+ font-size: 12px;
128
+ color: var(--muted);
129
+ }
130
+
131
+ .empty {
132
+ color: var(--muted);
133
+ font-size: 13px;
134
+ padding: 8px 4px;
135
+ }
136
+
137
+ .main {
138
+ margin-left: var(--sidebar-width);
139
+ height: 100vh;
140
+ }
141
+
142
+ .header {
143
+ position: fixed;
144
+ top: 0;
145
+ left: var(--sidebar-width);
146
+ right: 0;
147
+ height: var(--header-height);
148
+ z-index: 20;
149
+ border-bottom: 1px solid var(--line);
150
+ background: rgba(255, 255, 255, 0.75);
151
+ backdrop-filter: blur(6px);
152
+ padding: 14px 20px;
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: center;
156
+ gap: 12px;
157
+ }
158
+
159
+ #activeTitle {
160
+ margin: 0;
161
+ font-size: 17px;
162
+ font-weight: 700;
163
+ }
164
+
165
+ .header-actions {
166
+ display: flex;
167
+ gap: 8px;
168
+ }
169
+
170
+ #messages {
171
+ position: fixed;
172
+ left: var(--sidebar-width);
173
+ right: 0;
174
+ top: var(--header-height);
175
+ bottom: var(--composer-height);
176
+ overflow-y: auto;
177
+ scroll-behavior: smooth;
178
+ padding: 18px 20px;
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: 14px;
182
+ }
183
+
184
+ #messages::-webkit-scrollbar {
185
+ width: 10px;
186
+ }
187
+
188
+ #messages::-webkit-scrollbar-thumb {
189
+ background: #bdd8c8;
190
+ border-radius: 8px;
191
+ }
192
+
193
+ .msg {
194
+ display: flex;
195
+ flex-direction: column;
196
+ max-width: 920px;
197
+ width: fit-content;
198
+ }
199
+
200
+ .msg.user {
201
+ align-self: flex-end;
202
+ }
203
+
204
+ .msg.assistant,
205
+ .msg.system {
206
+ align-self: flex-start;
207
+ }
208
+
209
+ .role {
210
+ font-size: 12px;
211
+ color: var(--muted);
212
+ margin-bottom: 4px;
213
+ font-weight: 600;
214
+ }
215
+
216
+ .bubble {
217
+ border: 1px solid var(--line);
218
+ background: var(--assistant-bubble);
219
+ border-radius: 12px;
220
+ padding: 10px 12px;
221
+ }
222
+
223
+ .msg.user .bubble {
224
+ background: var(--user-bubble);
225
+ }
226
+
227
+ .msg.system .bubble {
228
+ background: var(--system-bubble);
229
+ }
230
+
231
+ .bubble pre {
232
+ margin: 0;
233
+ white-space: pre-wrap;
234
+ word-break: break-word;
235
+ font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
236
+ font-size: 13px;
237
+ line-height: 1.5;
238
+ }
239
+
240
+ .composer {
241
+ position: fixed;
242
+ left: var(--sidebar-width);
243
+ right: 0;
244
+ bottom: 0;
245
+ min-height: var(--composer-height);
246
+ z-index: 20;
247
+ border-top: 1px solid var(--line);
248
+ padding: 12px 20px 16px;
249
+ background: rgba(255, 255, 255, 0.86);
250
+ }
251
+
252
+ .composer-inner {
253
+ display: flex;
254
+ gap: 10px;
255
+ align-items: flex-end;
256
+ }
257
+
258
+ #commandInput {
259
+ width: 100%;
260
+ min-height: 120px;
261
+ height: 120px;
262
+ max-height: 300px;
263
+ border: 1px solid var(--line);
264
+ border-radius: 12px;
265
+ padding: 11px 12px;
266
+ resize: none;
267
+ font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
268
+ font-size: 13px;
269
+ background: #fff;
270
+ }
271
+
272
+ #sendBtn[disabled] {
273
+ opacity: 0.6;
274
+ cursor: not-allowed;
275
+ }
276
+
277
+ @media (max-width: 900px) {
278
+ body {
279
+ height: auto;
280
+ overflow: auto;
281
+ }
282
+
283
+ .sidebar {
284
+ position: static;
285
+ width: auto;
286
+ z-index: auto;
287
+ border-right: none;
288
+ border-bottom: 1px solid var(--line);
289
+ max-height: 42vh;
290
+ }
291
+
292
+ .main {
293
+ margin-left: 0;
294
+ min-height: 58vh;
295
+ height: auto;
296
+ display: grid;
297
+ grid-template-rows: auto 1fr auto;
298
+ }
299
+
300
+ .header {
301
+ position: static;
302
+ height: auto;
303
+ }
304
+
305
+ #messages {
306
+ position: static;
307
+ min-height: 42vh;
308
+ max-height: 42vh;
309
+ }
310
+
311
+ .composer {
312
+ position: static;
313
+ min-height: auto;
314
+ }
315
+ }
@@ -0,0 +1,40 @@
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" />
6
+ <title>MANYOYO Web</title>
7
+ <link rel="stylesheet" href="/app/static/app.css" />
8
+ </head>
9
+ <body>
10
+ <div class="app">
11
+ <aside class="sidebar">
12
+ <div class="brand">MANYOYO Web</div>
13
+ <form class="new-session" id="newSessionForm">
14
+ <input id="newSessionName" placeholder="容器名 (例如 myy-dev)" />
15
+ <button type="submit">新建</button>
16
+ </form>
17
+ <div id="sessionList"></div>
18
+ </aside>
19
+ <main class="main">
20
+ <header class="header">
21
+ <h1 id="activeTitle">未选择会话</h1>
22
+ <div class="header-actions">
23
+ <button type="button" id="refreshBtn" class="secondary">刷新</button>
24
+ <button type="button" id="removeBtn" class="secondary">删除容器</button>
25
+ <button type="button" id="removeAllBtn" class="secondary">删除容器与聊天记录</button>
26
+ </div>
27
+ </header>
28
+ <section id="messages"></section>
29
+ <form class="composer" id="composer">
30
+ <div class="composer-inner">
31
+ <textarea id="commandInput" placeholder="输入容器命令,例如: ls -la"></textarea>
32
+ <button type="submit" id="sendBtn">发送</button>
33
+ </div>
34
+ </form>
35
+ </main>
36
+ </div>
37
+
38
+ <script src="/app/static/app.js"></script>
39
+ </body>
40
+ </html>
@@ -0,0 +1,276 @@
1
+ (function () {
2
+ const state = {
3
+ sessions: [],
4
+ active: '',
5
+ messages: [],
6
+ sending: false
7
+ };
8
+
9
+ const sessionList = document.getElementById('sessionList');
10
+ const activeTitle = document.getElementById('activeTitle');
11
+ const messagesNode = document.getElementById('messages');
12
+ const newSessionForm = document.getElementById('newSessionForm');
13
+ const newSessionName = document.getElementById('newSessionName');
14
+ const composer = document.getElementById('composer');
15
+ const commandInput = document.getElementById('commandInput');
16
+ const sendBtn = document.getElementById('sendBtn');
17
+ const refreshBtn = document.getElementById('refreshBtn');
18
+ const removeBtn = document.getElementById('removeBtn');
19
+ const removeAllBtn = document.getElementById('removeAllBtn');
20
+
21
+ function roleName(role) {
22
+ if (role === 'user') return '你';
23
+ if (role === 'assistant') return '容器输出';
24
+ return '系统';
25
+ }
26
+
27
+ function formatStatus(status) {
28
+ if (!status) return 'history';
29
+ return status;
30
+ }
31
+
32
+ async function api(url, options) {
33
+ const requestOptions = Object.assign(
34
+ { headers: { 'Content-Type': 'application/json' } },
35
+ options || {}
36
+ );
37
+ const response = await fetch(url, requestOptions);
38
+ if (response.status === 401) {
39
+ window.location.href = '/';
40
+ throw new Error('未登录或登录已过期');
41
+ }
42
+ let data = {};
43
+ try {
44
+ data = await response.json();
45
+ } catch (e) {
46
+ data = {};
47
+ }
48
+ if (!response.ok) {
49
+ throw new Error(data.error || '请求失败');
50
+ }
51
+ return data;
52
+ }
53
+
54
+ function setSending(value) {
55
+ state.sending = value;
56
+ sendBtn.disabled = value || !state.active;
57
+ commandInput.disabled = !state.active;
58
+ }
59
+
60
+ function updateHeader() {
61
+ if (!state.active) {
62
+ activeTitle.textContent = '未选择会话';
63
+ removeBtn.disabled = true;
64
+ removeAllBtn.disabled = true;
65
+ setSending(false);
66
+ commandInput.value = '';
67
+ return;
68
+ }
69
+ activeTitle.textContent = state.active;
70
+ removeBtn.disabled = false;
71
+ removeAllBtn.disabled = false;
72
+ setSending(state.sending);
73
+ }
74
+
75
+ function renderSessions() {
76
+ sessionList.innerHTML = '';
77
+ if (!state.sessions.length) {
78
+ const empty = document.createElement('div');
79
+ empty.className = 'empty';
80
+ empty.textContent = '暂无 manyoyo 会话';
81
+ sessionList.appendChild(empty);
82
+ return;
83
+ }
84
+
85
+ state.sessions.forEach(function (session) {
86
+ const btn = document.createElement('button');
87
+ btn.type = 'button';
88
+ btn.className = 'session-item' + (state.active === session.name ? ' active' : '');
89
+ btn.innerHTML =
90
+ '<div class="session-name">' + session.name + '</div>' +
91
+ '<div class="session-meta">' + formatStatus(session.status) + '</div>';
92
+ btn.addEventListener('click', function () {
93
+ state.active = session.name;
94
+ updateHeader();
95
+ renderSessions();
96
+ loadMessages();
97
+ });
98
+ sessionList.appendChild(btn);
99
+ });
100
+ }
101
+
102
+ function renderMessages(messages) {
103
+ messagesNode.innerHTML = '';
104
+ if (!messages.length) {
105
+ const empty = document.createElement('div');
106
+ empty.className = 'empty';
107
+ empty.textContent = '输入命令后,容器输出会显示在这里。';
108
+ messagesNode.appendChild(empty);
109
+ return;
110
+ }
111
+
112
+ messages.forEach(function (msg) {
113
+ const row = document.createElement('article');
114
+ row.className = 'msg ' + (msg.role || 'system');
115
+
116
+ const role = document.createElement('div');
117
+ role.className = 'role';
118
+ role.textContent = roleName(msg.role);
119
+
120
+ const bubble = document.createElement('div');
121
+ bubble.className = 'bubble';
122
+
123
+ const pre = document.createElement('pre');
124
+ pre.textContent = msg.content || '';
125
+ bubble.appendChild(pre);
126
+
127
+ row.appendChild(role);
128
+ row.appendChild(bubble);
129
+ messagesNode.appendChild(row);
130
+ });
131
+
132
+ messagesNode.scrollTop = messagesNode.scrollHeight;
133
+ }
134
+
135
+ async function loadSessions(preferredName) {
136
+ const data = await api('/api/sessions');
137
+ state.sessions = Array.isArray(data.sessions) ? data.sessions : [];
138
+
139
+ if (preferredName) {
140
+ state.active = preferredName;
141
+ }
142
+
143
+ if (state.active && !state.sessions.some(function (s) { return s.name === state.active; })) {
144
+ state.active = '';
145
+ }
146
+
147
+ if (!state.active && state.sessions.length) {
148
+ state.active = state.sessions[0].name;
149
+ }
150
+
151
+ updateHeader();
152
+ renderSessions();
153
+ await loadMessages();
154
+ }
155
+
156
+ async function loadMessages() {
157
+ if (!state.active) {
158
+ state.messages = [];
159
+ renderMessages(state.messages);
160
+ return;
161
+ }
162
+ const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/messages');
163
+ state.messages = Array.isArray(data.messages) ? data.messages : [];
164
+ renderMessages(state.messages);
165
+ }
166
+
167
+ newSessionForm.addEventListener('submit', async function (event) {
168
+ event.preventDefault();
169
+ try {
170
+ const name = (newSessionName.value || '').trim();
171
+ const data = await api('/api/sessions', {
172
+ method: 'POST',
173
+ body: JSON.stringify({ name: name })
174
+ });
175
+ newSessionName.value = '';
176
+ await loadSessions(data.name);
177
+ } catch (e) {
178
+ alert(e.message);
179
+ }
180
+ });
181
+
182
+ composer.addEventListener('submit', async function (event) {
183
+ event.preventDefault();
184
+ if (!state.active) return;
185
+ if (state.sending) return;
186
+ const command = (commandInput.value || '').trim();
187
+ if (!command) return;
188
+
189
+ const submitSession = state.active;
190
+ const previousMessages = state.messages.slice();
191
+ state.messages = state.messages.concat([{
192
+ role: 'user',
193
+ content: command,
194
+ timestamp: new Date().toISOString(),
195
+ pending: true
196
+ }]);
197
+ renderMessages(state.messages);
198
+
199
+ setSending(true);
200
+ try {
201
+ commandInput.value = '';
202
+ commandInput.focus();
203
+ await api('/api/sessions/' + encodeURIComponent(submitSession) + '/run', {
204
+ method: 'POST',
205
+ body: JSON.stringify({ command: command })
206
+ });
207
+ await loadSessions(submitSession);
208
+ } catch (e) {
209
+ if (state.active === submitSession) {
210
+ state.messages = previousMessages;
211
+ renderMessages(state.messages);
212
+ }
213
+ alert(e.message);
214
+ } finally {
215
+ setSending(false);
216
+ commandInput.focus();
217
+ }
218
+ });
219
+
220
+ commandInput.addEventListener('keydown', function (event) {
221
+ if (event.key !== 'Enter' || event.isComposing) {
222
+ return;
223
+ }
224
+
225
+ // Shift+Enter / Option(Alt)+Enter: 换行
226
+ if (event.shiftKey || event.altKey) {
227
+ return;
228
+ }
229
+
230
+ // Enter / Ctrl+Enter: 发送
231
+ event.preventDefault();
232
+ if (!state.active || state.sending) {
233
+ return;
234
+ }
235
+ composer.requestSubmit();
236
+ });
237
+
238
+ refreshBtn.addEventListener('click', function () {
239
+ loadSessions(state.active).catch(function (e) { alert(e.message); });
240
+ });
241
+
242
+ removeBtn.addEventListener('click', async function () {
243
+ if (!state.active) return;
244
+ const yes = confirm('确认删除容器 ' + state.active + ' ? 仅删除容器,历史消息仍保留。');
245
+ if (!yes) return;
246
+ try {
247
+ const current = state.active;
248
+ await api('/api/sessions/' + encodeURIComponent(current) + '/remove', {
249
+ method: 'POST'
250
+ });
251
+ await loadSessions('');
252
+ } catch (e) {
253
+ alert(e.message);
254
+ }
255
+ });
256
+
257
+ removeAllBtn.addEventListener('click', async function () {
258
+ if (!state.active) return;
259
+ const yes = confirm('确认删除容器和聊天记录 ' + state.active + ' ? 删除后无法恢复。');
260
+ if (!yes) return;
261
+ try {
262
+ const current = state.active;
263
+ await api('/api/sessions/' + encodeURIComponent(current) + '/remove-with-history', {
264
+ method: 'POST'
265
+ });
266
+ await loadSessions('');
267
+ } catch (e) {
268
+ alert(e.message);
269
+ }
270
+ });
271
+
272
+ setSending(false);
273
+ loadSessions().catch(function (e) {
274
+ alert(e.message);
275
+ });
276
+ })();
@@ -0,0 +1,81 @@
1
+ :root {
2
+ --bg: #f4f7f5;
3
+ --panel: #ffffff;
4
+ --line: #dbe4de;
5
+ --text: #0f2f20;
6
+ --muted: #4a6256;
7
+ --accent: #0f9d58;
8
+ --accent-strong: #087f45;
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ margin: 0;
17
+ min-height: 100vh;
18
+ display: grid;
19
+ place-items: center;
20
+ font-family: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
21
+ color: var(--text);
22
+ background: radial-gradient(circle at 0 0, #d8efe2 0%, var(--bg) 45%, #f5f8f6 100%);
23
+ }
24
+
25
+ .card {
26
+ width: min(420px, calc(100vw - 32px));
27
+ background: var(--panel);
28
+ border: 1px solid var(--line);
29
+ border-radius: 14px;
30
+ padding: 24px;
31
+ }
32
+
33
+ h1 {
34
+ margin: 0 0 6px;
35
+ font-size: 22px;
36
+ }
37
+
38
+ p {
39
+ margin: 0 0 18px;
40
+ color: var(--muted);
41
+ font-size: 13px;
42
+ }
43
+
44
+ label {
45
+ display: block;
46
+ margin: 10px 0 6px;
47
+ font-size: 13px;
48
+ font-weight: 600;
49
+ }
50
+
51
+ input {
52
+ width: 100%;
53
+ border: 1px solid var(--line);
54
+ border-radius: 10px;
55
+ padding: 10px 11px;
56
+ font-size: 14px;
57
+ }
58
+
59
+ button {
60
+ width: 100%;
61
+ margin-top: 14px;
62
+ border: none;
63
+ border-radius: 10px;
64
+ padding: 10px 12px;
65
+ font-size: 14px;
66
+ font-weight: 700;
67
+ color: #fff;
68
+ background: var(--accent);
69
+ cursor: pointer;
70
+ }
71
+
72
+ button:hover {
73
+ background: var(--accent-strong);
74
+ }
75
+
76
+ .error {
77
+ margin-top: 10px;
78
+ min-height: 20px;
79
+ font-size: 13px;
80
+ color: #b00020;
81
+ }