create-openclaw-bot 5.8.0 → 5.8.2
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 +167 -159
- package/README.vi.md +172 -164
- package/dist/cli.js +125 -112
- package/dist/legacy-cli.js +11 -11
- package/dist/server/local-server.js +372 -70
- package/dist/setup/data/index.js +0 -1
- package/dist/setup/data/plugins.js +8 -1
- package/dist/setup/data/skills.js +2 -10
- package/dist/setup/shared/docker-gen.js +576 -576
- package/dist/setup/shared/workspace-gen.js +813 -526
- package/dist/setup.js +367 -324
- package/dist/web/app.js +1276 -1106
- package/dist/web/styles.css +1054 -286
- package/package.json +5 -5
package/dist/web/app.js
CHANGED
|
@@ -1,1106 +1,1276 @@
|
|
|
1
|
-
const $ = (sel) => document.querySelector(sel);
|
|
2
|
-
const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, installTab: 'docker', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, openDirs: {} };
|
|
3
|
-
const SVG_CDN = 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons';
|
|
4
|
-
const OS_OPTIONS = [
|
|
5
|
-
{ id: 'win', title: 'Windows', subtitle: 'Auto-detected desktop', icon: `${SVG_CDN}/windows/default.svg`, badge: 'Desktop' },
|
|
6
|
-
{ id: 'macos', title: 'macOS', subtitle: 'Apple Silicon / Intel', icon: `${SVG_CDN}/apple/default.svg`, badge: 'Desktop' },
|
|
7
|
-
{ id: 'linux-desktop', title: 'Linux Desktop', subtitle: 'Ubuntu / Debian / Fedora', icon: `${SVG_CDN}/linux/default.svg`, badge: 'Desktop' },
|
|
8
|
-
{ id: 'vps', title: 'Linux VPS', subtitle: 'Server install with public bind', icon: `${SVG_CDN}/ubuntu/default.svg`, badge: 'Server' },
|
|
9
|
-
];
|
|
10
|
-
const MODE_OPTIONS = [
|
|
11
|
-
{ id: 'docker', title: 'Docker', subtitle: 'Isolated containers, safest default', icon: `${SVG_CDN}/docker/default.svg`, badge: 'Recommended' },
|
|
12
|
-
{ id: 'native', title: 'Native', subtitle: 'Direct host install, lighter runtime', icon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%231b1113'/%3E%3Cpath d='M15 22l9 10-9 10' fill='none' stroke='%23ff3b4d' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M30 42h19' fill='none' stroke='%23f8f7f7' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E`, badge: 'Advanced' },
|
|
13
|
-
];
|
|
14
|
-
const BOT_CHANNELS = [
|
|
15
|
-
{ id: 'telegram', title: 'Telegram', subtitle: 'Bot API', icon: `${SVG_CDN}/telegram/default.svg`, badge: 'Tele' },
|
|
16
|
-
{ id: 'zalo-personal', title: 'Zalo User', subtitle: 'Personal account', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'User' },
|
|
17
|
-
{ id: 'zalo-bot', title: 'Zalo API', subtitle: 'Official Account', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'API' },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
function choiceCard(group, item, current) {
|
|
21
|
-
return `<label class="choice-card logo-card ${item.id === current ? 'is-selected' : ''}">
|
|
22
|
-
<input name="${group}" type="radio" value="${item.id}" ${item.id===current?'checked':''}/>
|
|
23
|
-
<span class="choice-card__icon"><img src="${item.icon}" alt="${item.title} icon" loading="lazy" onerror="this.style.display='none'"/></span>
|
|
24
|
-
<span class="choice-card__body"><strong>${item.title}</strong><small>${item.badge}</small></span>
|
|
25
|
-
</label>`;
|
|
26
|
-
}
|
|
27
|
-
function staticChoiceCard(item) {
|
|
28
|
-
return `<div class="choice-card logo-card is-selected bot-channel-static" aria-label="${escapeHtml(item.title)}">
|
|
29
|
-
<span class="choice-card__icon"><img src="${item.icon}" alt="${escapeHtml(item.title)} icon" loading="lazy" onerror="this.style.display='none'"/></span>
|
|
30
|
-
<span class="choice-card__body"><strong>${escapeHtml(item.title)}</strong><small>${escapeHtml(item.badge)}</small></span>
|
|
31
|
-
</div>`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function api(path, opts) {
|
|
35
|
-
const res = await fetch(path, opts && opts.body ? { ...opts, headers: { 'content-type': 'application/json' }, body: JSON.stringify(opts.body) } : opts);
|
|
36
|
-
if (!res.ok) throw new Error((await res.json()).error || res.statusText);
|
|
37
|
-
return res.json();
|
|
38
|
-
}
|
|
39
|
-
function runtimeBadge(label, kind='') { return `<span class="mini-pill ${kind?`mini-pill--${kind}`:''}">${escapeHtml(label)}</span>`; }
|
|
40
|
-
async function withButtonLoading(btn, task) {
|
|
41
|
-
if (!btn || btn.classList.contains('is-loading')) return;
|
|
42
|
-
const prevDisabled = btn.disabled;
|
|
43
|
-
btn.classList.add('is-loading');
|
|
44
|
-
btn.disabled = true;
|
|
45
|
-
btn.setAttribute('aria-busy', 'true');
|
|
46
|
-
try { return await task(); }
|
|
47
|
-
finally {
|
|
48
|
-
if (btn.isConnected) {
|
|
49
|
-
btn.classList.remove('is-loading');
|
|
50
|
-
btn.disabled = prevDisabled;
|
|
51
|
-
btn.removeAttribute('aria-busy');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
document.addEventListener('click', (ev) => {
|
|
56
|
-
const btn = ev.target.closest('button');
|
|
57
|
-
if (!btn || btn.disabled || btn.classList.contains('is-loading')) return;
|
|
58
|
-
btn.classList.add('is-press-loading');
|
|
59
|
-
setTimeout(() => btn.classList.remove('is-press-loading'), 450);
|
|
60
|
-
}, true);
|
|
61
|
-
|
|
62
|
-
function icon(name) {
|
|
63
|
-
const paths = {
|
|
64
|
-
dashboard: 'M4 5h7v7H4z M13 5h7v10h-7z M4 14h7v5H4z M13 17h7v2h-7z',
|
|
65
|
-
setup: 'M12 6v6l4 2M4 13a8 8 0 1 0 16 0 8 8 0 0 0-16 0Z',
|
|
66
|
-
bot: 'M7 8h10v8H7z M9 4h6v4H9z M9 11h.01M15 11h.01M10 16h4',
|
|
67
|
-
files: 'M6 3h8l4 4v14H6z M14 3v5h5 M9 13h6 M9 17h6',
|
|
68
|
-
skills: 'M12 2l2.4 6.8H22l-6 4.4 2.3 6.8-6.3-4.2L5.7 20 8 13.2 2 8.8h7.6z',
|
|
69
|
-
logs: 'M4 6h16M4 12h16M4 18h10'
|
|
70
|
-
};
|
|
71
|
-
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="${paths[name]}"/></svg>`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
function copyIcon() {
|
|
76
|
-
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
77
|
-
}
|
|
78
|
-
function actionIcon(name) {
|
|
79
|
-
const d = {
|
|
80
|
-
refresh: '<path d="M20 11a8 8 0 1 0 2 5.3"/><path d="M20 4v7h-7"/>',
|
|
81
|
-
folder: '<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5z"/>',
|
|
82
|
-
link: '<path d="M10 13a5 5 0 0 1 0-7l1.5-1.5a5 5 0 0 1 7 7L17 13"/><path d="M14 11a5 5 0 0 1 0 7L12.5 19.5a5 5 0 0 1-7-7L7 11"/>',
|
|
83
|
-
trash: '<path d="M4 7h16"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"/><path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/>',
|
|
84
|
-
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
|
85
|
-
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
|
|
86
|
-
edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/>',
|
|
87
|
-
spark: '<path d="M12 2l1.8 5.2L19 9l-5.2 1.8L12 16l-1.8-5.2L5 9l5.2-1.8Z"/>'
|
|
88
|
-
}[name];
|
|
89
|
-
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
|
|
90
|
-
}
|
|
91
|
-
function socialIcon(name) {
|
|
92
|
-
const d = {
|
|
93
|
-
facebook:'M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z',
|
|
94
|
-
telegram:'M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z',
|
|
95
|
-
zalo:'M4 6h16v10H8l-4 4V6z M9 10h6 M9 13h4',
|
|
96
|
-
github:'M9 19c-5 1-5-2-7-3m14 6v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.1-1.5 6.1-6.7A5.2 5.2 0 0 0 20 5.2 4.8 4.8 0 0 0 19.9 2S18.7 1.7 16 3.5a13.4 13.4 0 0 0-7 0C6.3 1.7 5.1 2 5.1 2A4.8 4.8 0 0 0 5 5.2a5.2 5.2 0 0 0-1.4 3.6c0 5.2 3.1 6.4 6.1 6.7a3 3 0 0 0-.9 2.1V22'
|
|
97
|
-
}[name];
|
|
98
|
-
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
99
|
-
}
|
|
100
|
-
function sidebarExtras() {
|
|
101
|
-
const socials = [
|
|
102
|
-
['facebook','https://www.facebook.com/holeminhtuan.it/'],
|
|
103
|
-
['telegram','https://t.me/holeminhtuan_it'],
|
|
104
|
-
['zalo','https://
|
|
105
|
-
['github','https://github.com/tuanminhhole/']
|
|
106
|
-
];
|
|
107
|
-
return `<div
|
|
108
|
-
<
|
|
109
|
-
<div class="
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
<div class="install-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
function
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
<div
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
function
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
<div class="
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
{
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}))
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1
|
+
const $ = (sel) => document.querySelector(sel);
|
|
2
|
+
const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, installTab: 'docker', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, openDirs: {} };
|
|
3
|
+
const SVG_CDN = 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons';
|
|
4
|
+
const OS_OPTIONS = [
|
|
5
|
+
{ id: 'win', title: 'Windows', subtitle: 'Auto-detected desktop', icon: `${SVG_CDN}/windows/default.svg`, badge: 'Desktop' },
|
|
6
|
+
{ id: 'macos', title: 'macOS', subtitle: 'Apple Silicon / Intel', icon: `${SVG_CDN}/apple/default.svg`, badge: 'Desktop' },
|
|
7
|
+
{ id: 'linux-desktop', title: 'Linux Desktop', subtitle: 'Ubuntu / Debian / Fedora', icon: `${SVG_CDN}/linux/default.svg`, badge: 'Desktop' },
|
|
8
|
+
{ id: 'vps', title: 'Linux VPS', subtitle: 'Server install with public bind', icon: `${SVG_CDN}/ubuntu/default.svg`, badge: 'Server' },
|
|
9
|
+
];
|
|
10
|
+
const MODE_OPTIONS = [
|
|
11
|
+
{ id: 'docker', title: 'Docker', subtitle: 'Isolated containers, safest default', icon: `${SVG_CDN}/docker/default.svg`, badge: 'Recommended' },
|
|
12
|
+
{ id: 'native', title: 'Native', subtitle: 'Direct host install, lighter runtime', icon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%231b1113'/%3E%3Cpath d='M15 22l9 10-9 10' fill='none' stroke='%23ff3b4d' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M30 42h19' fill='none' stroke='%23f8f7f7' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E`, badge: 'Advanced' },
|
|
13
|
+
];
|
|
14
|
+
const BOT_CHANNELS = [
|
|
15
|
+
{ id: 'telegram', title: 'Telegram', subtitle: 'Bot API', icon: `${SVG_CDN}/telegram/default.svg`, badge: 'Tele' },
|
|
16
|
+
{ id: 'zalo-personal', title: 'Zalo User', subtitle: 'Personal account', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'User' },
|
|
17
|
+
{ id: 'zalo-bot', title: 'Zalo API', subtitle: 'Official Account', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'API' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function choiceCard(group, item, current) {
|
|
21
|
+
return `<label class="choice-card logo-card ${item.id === current ? 'is-selected' : ''}">
|
|
22
|
+
<input name="${group}" type="radio" value="${item.id}" ${item.id===current?'checked':''}/>
|
|
23
|
+
<span class="choice-card__icon"><img src="${item.icon}" alt="${item.title} icon" loading="lazy" onerror="this.style.display='none'"/></span>
|
|
24
|
+
<span class="choice-card__body"><strong>${item.title}</strong><small>${item.badge}</small></span>
|
|
25
|
+
</label>`;
|
|
26
|
+
}
|
|
27
|
+
function staticChoiceCard(item) {
|
|
28
|
+
return `<div class="choice-card logo-card is-selected bot-channel-static" aria-label="${escapeHtml(item.title)}">
|
|
29
|
+
<span class="choice-card__icon"><img src="${item.icon}" alt="${escapeHtml(item.title)} icon" loading="lazy" onerror="this.style.display='none'"/></span>
|
|
30
|
+
<span class="choice-card__body"><strong>${escapeHtml(item.title)}</strong><small>${escapeHtml(item.badge)}</small></span>
|
|
31
|
+
</div>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function api(path, opts) {
|
|
35
|
+
const res = await fetch(path, opts && opts.body ? { ...opts, headers: { 'content-type': 'application/json' }, body: JSON.stringify(opts.body) } : opts);
|
|
36
|
+
if (!res.ok) throw new Error((await res.json()).error || res.statusText);
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
function runtimeBadge(label, kind='') { return `<span class="mini-pill ${kind?`mini-pill--${kind}`:''}">${escapeHtml(label)}</span>`; }
|
|
40
|
+
async function withButtonLoading(btn, task) {
|
|
41
|
+
if (!btn || btn.classList.contains('is-loading')) return;
|
|
42
|
+
const prevDisabled = btn.disabled;
|
|
43
|
+
btn.classList.add('is-loading');
|
|
44
|
+
btn.disabled = true;
|
|
45
|
+
btn.setAttribute('aria-busy', 'true');
|
|
46
|
+
try { return await task(); }
|
|
47
|
+
finally {
|
|
48
|
+
if (btn.isConnected) {
|
|
49
|
+
btn.classList.remove('is-loading');
|
|
50
|
+
btn.disabled = prevDisabled;
|
|
51
|
+
btn.removeAttribute('aria-busy');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
document.addEventListener('click', (ev) => {
|
|
56
|
+
const btn = ev.target.closest('button');
|
|
57
|
+
if (!btn || btn.disabled || btn.classList.contains('is-loading')) return;
|
|
58
|
+
btn.classList.add('is-press-loading');
|
|
59
|
+
setTimeout(() => btn.classList.remove('is-press-loading'), 450);
|
|
60
|
+
}, true);
|
|
61
|
+
|
|
62
|
+
function icon(name) {
|
|
63
|
+
const paths = {
|
|
64
|
+
dashboard: 'M4 5h7v7H4z M13 5h7v10h-7z M4 14h7v5H4z M13 17h7v2h-7z',
|
|
65
|
+
setup: 'M12 6v6l4 2M4 13a8 8 0 1 0 16 0 8 8 0 0 0-16 0Z',
|
|
66
|
+
bot: 'M7 8h10v8H7z M9 4h6v4H9z M9 11h.01M15 11h.01M10 16h4',
|
|
67
|
+
files: 'M6 3h8l4 4v14H6z M14 3v5h5 M9 13h6 M9 17h6',
|
|
68
|
+
skills: 'M12 2l2.4 6.8H22l-6 4.4 2.3 6.8-6.3-4.2L5.7 20 8 13.2 2 8.8h7.6z',
|
|
69
|
+
logs: 'M4 6h16M4 12h16M4 18h10'
|
|
70
|
+
};
|
|
71
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="${paths[name]}"/></svg>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function copyIcon() {
|
|
76
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
77
|
+
}
|
|
78
|
+
function actionIcon(name) {
|
|
79
|
+
const d = {
|
|
80
|
+
refresh: '<path d="M20 11a8 8 0 1 0 2 5.3"/><path d="M20 4v7h-7"/>',
|
|
81
|
+
folder: '<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5z"/>',
|
|
82
|
+
link: '<path d="M10 13a5 5 0 0 1 0-7l1.5-1.5a5 5 0 0 1 7 7L17 13"/><path d="M14 11a5 5 0 0 1 0 7L12.5 19.5a5 5 0 0 1-7-7L7 11"/>',
|
|
83
|
+
trash: '<path d="M4 7h16"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"/><path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/>',
|
|
84
|
+
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
|
85
|
+
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
|
|
86
|
+
edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/>',
|
|
87
|
+
spark: '<path d="M12 2l1.8 5.2L19 9l-5.2 1.8L12 16l-1.8-5.2L5 9l5.2-1.8Z"/>'
|
|
88
|
+
}[name];
|
|
89
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
|
|
90
|
+
}
|
|
91
|
+
function socialIcon(name) {
|
|
92
|
+
const d = {
|
|
93
|
+
facebook:'M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z',
|
|
94
|
+
telegram:'M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z',
|
|
95
|
+
zalo:'M4 6h16v10H8l-4 4V6z M9 10h6 M9 13h4',
|
|
96
|
+
github:'M9 19c-5 1-5-2-7-3m14 6v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.1-1.5 6.1-6.7A5.2 5.2 0 0 0 20 5.2 4.8 4.8 0 0 0 19.9 2S18.7 1.7 16 3.5a13.4 13.4 0 0 0-7 0C6.3 1.7 5.1 2 5.1 2A4.8 4.8 0 0 0 5 5.2a5.2 5.2 0 0 0-1.4 3.6c0 5.2 3.1 6.4 6.1 6.7a3 3 0 0 0-.9 2.1V22'
|
|
97
|
+
}[name];
|
|
98
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
99
|
+
}
|
|
100
|
+
function sidebarExtras() {
|
|
101
|
+
const socials = [
|
|
102
|
+
['facebook','https://www.facebook.com/holeminhtuan.it/'],
|
|
103
|
+
['telegram','https://t.me/holeminhtuan_it'],
|
|
104
|
+
['zalo','https://zalo.me/0962794917'],
|
|
105
|
+
['github','https://github.com/tuanminhhole/']
|
|
106
|
+
];
|
|
107
|
+
return `<div style="margin-top: auto; width: 100%;">
|
|
108
|
+
<hr style="border: 0; border-top: 1px solid var(--hair); margin: 16px 0 20px 0; opacity: 0.6;" />
|
|
109
|
+
<div class="sidebar-extra" style="margin-top: 0;">
|
|
110
|
+
<div class="side-info side-author" style="text-align: center; background: transparent; border: none; box-shadow: none; padding: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%;">
|
|
111
|
+
<p style="margin: 0 0 10px 0; font-weight: 600; color: var(--muted); font-size: 12.5px; display: inline-flex; align-items: center; gap: 4px;">Được làm ❤️ bởi <a href="https://zalo.me/0962794917" target="_blank" rel="noopener" style="color: var(--muted); text-decoration: none; font-weight: 700;">tuanminhole</a></p>
|
|
112
|
+
<div class="socials" style="justify-content: center; margin-top: 0; display: flex; gap: 8px; width: 100%;">
|
|
113
|
+
${socials.map(([n,u])=>`<a href="${u}" target="_blank" rel="noopener" aria-label="${n}">${socialIcon(n)}</a>`).join('')}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>`;
|
|
118
|
+
}
|
|
119
|
+
function donateModal() {
|
|
120
|
+
if (!state.donateOpen) return '';
|
|
121
|
+
return `<div class="modal-backdrop" data-donate="close"><section class="donate-modal" role="dialog" aria-modal="true" aria-label="Donate" onclick="event.stopPropagation()">
|
|
122
|
+
<button class="modal-x" data-donate="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
123
|
+
<div class="donate-head"><span aria-hidden="true">❤</span><div><p>${t('\u1ee6ng h\u1ed9 OpenClaw', 'Support OpenClaw')}</p><h2>Donate</h2><small>${t('\u0110\u00f3ng g\u00f3p c\u1ee7a b\u1ea1n gi\u00fap duy tr\u00ec h\u1ea1 t\u1ea7ng, s\u1eeda l\u1ed7i v\u00e0 c\u1ea3i ti\u1ebfn OpenClaw m\u1ed7i ng\u00e0y.', 'Your support keeps infrastructure running, fixes bugs, and improves OpenClaw every day.')}</small></div></div>
|
|
124
|
+
<div class="donate-grid"><article><div class="qr-frame"><img src="/bvvbank.jpg" alt="BVBank transfer info"></div><b>BVBank</b></article><article><div class="qr-frame"><img src="/momo.jpg" alt="Momo transfer info"></div><b>Momo</b></article></div>
|
|
125
|
+
</section></div>`;
|
|
126
|
+
}
|
|
127
|
+
function confirmModal() {
|
|
128
|
+
const m = state.confirmModal;
|
|
129
|
+
if (!m) return '';
|
|
130
|
+
return `<div class="modal-backdrop confirm-backdrop" data-confirm-action="cancel">
|
|
131
|
+
<section class="donate-modal confirm-modal" role="dialog" aria-modal="true" aria-label="${escapeHtml(m.title)}" onclick="event.stopPropagation()">
|
|
132
|
+
<button class="modal-x" data-confirm-action="cancel" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
133
|
+
<div class="donate-head"><span aria-hidden="true">⚠</span><div><p>${escapeHtml(m.eyebrow || t('Xác nhận','Confirm'))}</p><h2>${escapeHtml(m.title)}</h2><small>${escapeHtml(m.message || '')}</small></div></div>
|
|
134
|
+
<div class="confirm-actions"><button class="secondary" data-confirm-action="cancel">${t('Hủy','Cancel')}</button><button class="primary danger" data-confirm-action="ok">${escapeHtml(m.okText || t('Xóa','Delete'))}</button></div>
|
|
135
|
+
</section>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
138
|
+
function pathModal() {
|
|
139
|
+
const m = state.pathModal;
|
|
140
|
+
if (!m) return '';
|
|
141
|
+
return `<div class="modal-backdrop confirm-backdrop" data-path-action="cancel">
|
|
142
|
+
<section class="donate-modal confirm-modal" role="dialog" aria-modal="true" aria-label="${escapeHtml(m.title || '')}" onclick="event.stopPropagation()">
|
|
143
|
+
<button class="modal-x" data-path-action="cancel" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
144
|
+
<div class="donate-head"><span aria-hidden="true">📁</span><div><p>${escapeHtml(m.eyebrow || t('Nhập đường dẫn','Enter path'))}</p><h2>${escapeHtml(m.title || t('Đường dẫn project','Project path'))}</h2><small>${escapeHtml(m.message || '')}</small></div></div>
|
|
145
|
+
<label class="field wide"><input id="path-modal-input" value="${escapeHtml(m.value || '')}" placeholder="${escapeHtml(m.placeholder || '')}" /></label>
|
|
146
|
+
<div class="confirm-actions"><button class="secondary" data-path-action="cancel">${t('Hủy','Cancel')}</button><button class="primary" data-path-action="ok">${t('Xác nhận','Confirm')}</button></div>
|
|
147
|
+
</section>
|
|
148
|
+
</div>`;
|
|
149
|
+
}
|
|
150
|
+
function zaloLoginModal() {
|
|
151
|
+
if (!state.zaloLoginOpen) return '';
|
|
152
|
+
const lines = state.zaloLoginLines.slice(-120).join('\n') || t('Đang khởi động login Zalo...', 'Starting Zalo login...');
|
|
153
|
+
return `<div class="modal-backdrop zalo-login-backdrop" data-zalo-login="close">
|
|
154
|
+
<section class="donate-modal zalo-login-modal" role="dialog" aria-modal="true" aria-label="Zalo login" onclick="event.stopPropagation()">
|
|
155
|
+
<button class="modal-x" data-zalo-login="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
156
|
+
<div class="donate-head"><span aria-hidden="true">Z</span><div><p>Zalo User</p><h2>${t('Quét mã QR đăng nhập','Scan login QR')}</h2><small>${t('Mở Zalo trên điện thoại → quét QR trong khung dưới. Nếu QR chưa hiện, đợi vài giây.', 'Open Zalo on your phone → scan the QR below. If it is not visible yet, wait a few seconds.')}</small></div></div>
|
|
157
|
+
${state.zaloQrDataUrl ? `<div class="zalo-qr-image-wrap"><img class="zalo-qr-image" src="${state.zaloQrDataUrl}" alt="Zalo login QR"/></div>` : ''}
|
|
158
|
+
<pre class="zalo-qr-log" data-zalo-qr-log>${escapeHtml(lines)}</pre>
|
|
159
|
+
<div class="zalo-login-actions"><button class="secondary" data-zalo-login="close" type="button">${t('\u0110óng','Close')}</button></div>
|
|
160
|
+
</section>
|
|
161
|
+
</div>`;
|
|
162
|
+
}
|
|
163
|
+
function installModal() {
|
|
164
|
+
if (!state.installModalOpen) return '';
|
|
165
|
+
const sys = state.system || {};
|
|
166
|
+
const draft = refreshInstallDraft();
|
|
167
|
+
const os = draft.os || state.os || sys?.os || 'win';
|
|
168
|
+
const mode = draft.mode || state.installTab || state.mode || sys?.recommendedMode || 'docker';
|
|
169
|
+
const pathExample = os === 'win' ? 'E:\\bot' : os === 'macos' ? '/Users/you/openclaw-bot' : '/home/you/openclaw-bot';
|
|
170
|
+
const osChoices = OS_OPTIONS.map(o => [o.id, t(o.title, o.title), trChoice(o).subtitle]);
|
|
171
|
+
const modeChoices = [
|
|
172
|
+
['docker', 'Docker', t('\u0043ontainer c\u00f4 l\u1eadp, an to\u00e0n nh\u1ea5t', 'Isolated containers, safest default')],
|
|
173
|
+
['native', 'Native', t('C\u00e0i tr\u1ef1c ti\u1ebfp, runtime nh\u1eb9 h\u01a1n', 'Direct host install, lighter runtime')],
|
|
174
|
+
];
|
|
175
|
+
return `<div class="modal-backdrop install-backdrop" data-install-modal="close">
|
|
176
|
+
<section class="donate-modal install-modal" role="dialog" aria-modal="true" aria-label="${t('T\u1ea1o Project','Create Project')}" onclick="event.stopPropagation()">
|
|
177
|
+
<button class="modal-x" data-install-modal="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
178
|
+
<div class="donate-head"><span aria-hidden="true">+</span><div><p>${t('T\u1ea1o Project','Create Project')}</p><h2>${t('T\u1ea1o Project','Create Project')}</h2><small>${t('Ch\u1ecdn s\u1eb5n ch\u1ebf \u0111\u1ed9 \u1edf tab tr\u00ean. B\u00ean d\u01b0\u1edbi ch\u1ec9 c\u1ea7n ch\u1ecdn OS v\u00e0 nh\u1eadp ho\u1eb7c ch\u1ecdn \u0111\u01b0\u1eddng d\u1eabn project.','Mode stays in the tabs above. Below, choose OS and enter or pick the project path.')}</small></div></div>
|
|
179
|
+
<form id="install-form" class="install-form">
|
|
180
|
+
<div class="install-tabs">${modeChoices.map(([id,label,desc]) => `<button type="button" class="install-tab ${mode===id?'is-active':''}" data-install-set="mode" data-value="${id}"><strong>${escapeHtml(label)}</strong><small>${escapeHtml(desc)}</small></button>`).join('')}</div>
|
|
181
|
+
<div class="install-grid install-grid--compact">
|
|
182
|
+
<div class="field wide"><span>${t('H\u1ec7 \u0111i\u1ec1u h\u00e0nh','Operating system')}</span>${pillGroup('os', os, osChoices)}<small>${t('\u0110\u00e3 ch\u1ecdn s\u1eb5n theo m\u00e1y \u0111ang ch\u1ea1y','Preselected from the current machine')}</small></div>
|
|
183
|
+
<label class="field wide"><span>${t('Đường dẫn project','Project path')}</span><input name="projectDir" placeholder="${escapeHtml(pathExample)}" value="${escapeHtml(draft.projectDir || pathExample)}" /><small>${t('Ví dụ: E:\\bot hoặc /home/you/openclaw-bot. Bạn có thể tự sửa tên folder bot thành tên bất kỳ.','Example: E:\\bot or /home/you/openclaw-bot. You can rename folder bot to any name.')}</small></label>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="install-preview">${t('S\u1ebd t\u1ea1o t\u1ea1i','Will create at')} <code data-install-preview>${escapeHtml(draft.projectDir || pathExample)}</code></div>
|
|
186
|
+
<input type="hidden" name="os" value="${escapeHtml(os)}" />
|
|
187
|
+
<input type="hidden" name="mode" value="${escapeHtml(mode)}" />
|
|
188
|
+
<div class="install-actions"><button type="button" class="secondary" data-install-modal="close">${t('H\u1ee7y','Cancel')}</button><button type="submit" class="primary">${t('C\u00e0i \u0111\u1eb7t','Install')}</button></div>
|
|
189
|
+
${state.projectConnectMessage ? `<p class="bot-inline-msg">${escapeHtml(state.projectConnectMessage)}</p>` : ''}
|
|
190
|
+
</form>
|
|
191
|
+
</section>
|
|
192
|
+
</div>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function t(vi, en) { return state.lang === 'vi' ? vi : en; }
|
|
196
|
+
|
|
197
|
+
function ui(key) {
|
|
198
|
+
const m = {
|
|
199
|
+
setup:['C\u00e0i \u0111\u1eb7t','Setup'], bot:['Bot','Bot'], files:['T\u1ec7p','Files'], skills:['K\u1ef9 n\u0103ng','Skills'], logs:['Nh\u1eadt k\u00fd','Logs'],
|
|
200
|
+
localSetup:['C\u00e0i \u0111\u1eb7t c\u1ee5c b\u1ed9','Local Setup'], ready:['S\u1eb5n s\u00e0ng','Ready'], installed:['\u0110\u00e3 c\u00e0i','Installed'],
|
|
201
|
+
light:['S\u00e1ng','Light'], dark:['T\u1ed1i','Dark'], donate:['\u1ee6ng h\u1ed9','Donate'], installer:['TR\u00ccNH C\u00c0I \u0110\u1eb6T WEB C\u1ee4C B\u1ed8','LOCAL WEB INSTALLER'],
|
|
202
|
+
osTitle:['Ch\u1ecdn h\u1ec7 \u0111i\u1ec1u h\u00e0nh','Choose operating system'], osDesc:['M\u1eb7c \u0111\u1ecbnh theo m\u00e1y \u0111\u00e3 nh\u1eadn di\u1ec7n','Default follows detected machine'],
|
|
203
|
+
modeTitle:['Ch\u1ecdn ch\u1ebf \u0111\u1ed9 ch\u1ea1y','Choose runtime mode'], modeDesc:['Docker \u0111\u01b0\u1ee3c khuy\u00ean d\u00f9ng tr\u00ean Windows/macOS','Docker recommended on Windows/macOS'],
|
|
204
|
+
install:['C\u00e0i OpenClaw','Install OpenClaw'], installSub:['T\u1ea1o project \u2192 c\u00e0i runtime m\u1edbi nh\u1ea5t \u2192 kh\u1edfi \u0111\u1ed9ng bot','Generate project \u2192 install latest runtime \u2192 start bot'],
|
|
205
|
+
system:['H\u1ec7 th\u1ed1ng','System'], notReady:['Ch\u01b0a s\u1eb5n s\u00e0ng','Not ready'], missing:['Thi\u1ebfu','Missing'],
|
|
206
|
+
liveLogs:['Nh\u1eadt k\u00fd tr\u1ef1c ti\u1ebfp','Live Logs'], status:['Tr\u1ea1ng th\u00e1i','Status'], yes:['C\u00f3','Yes'], no:['Kh\u00f4ng','No'], mode:['Ch\u1ebf \u0111\u1ed9','Mode'], project:['Project','Project'], gateway:['Gateway','Gateway'],
|
|
207
|
+
next:['Ti\u1ebfp theo','Next'], nextDesc:['S\u1eeda file nh\u1eadn di\u1ec7n, sau \u0111\u00f3 c\u00e0i k\u1ef9 n\u0103ng/plugin.','Edit identity files, then install skills/plugins.'], openFiles:['M\u1edf t\u1ec7p','Open files'],
|
|
208
|
+
save:['L\u01b0u','Save'], saved:['\u0110\u00e3 l\u01b0u','Saved'], noFiles:['Ch\u01b0a c\u00f3 file markdown. H\u00e3y ch\u1ea1y c\u00e0i \u0111\u1eb7t tr\u01b0\u1edbc.','No markdown files yet. Run install first.'],
|
|
209
|
+
nativeCap:['N\u0103ng l\u1ef1c native','native capability'], plugins:['Plugins','Plugins'], installVerb:['C\u00e0i \u0111\u1eb7t','Install'],
|
|
210
|
+
desktop:['M\u00e1y b\u00e0n','Desktop'], server:['M\u00e1y ch\u1ee7','Server'], recommended:['Khuy\u00ean d\u00f9ng','Recommended'], advanced:['N\u00e2ng cao','Advanced']
|
|
211
|
+
}[key] || [key,key];
|
|
212
|
+
return t(m[0], m[1]);
|
|
213
|
+
}
|
|
214
|
+
function trChoice(item) {
|
|
215
|
+
const vi = {
|
|
216
|
+
'Auto-detected desktop':'M\u00e1y b\u00e0n t\u1ef1 nh\u1eadn di\u1ec7n', 'Apple Silicon / Intel':'Apple Silicon / Intel', 'Ubuntu / Debian / Fedora':'Ubuntu / Debian / Fedora', 'Server install with public bind':'C\u00e0i server v\u1edbi public bind',
|
|
217
|
+
'Isolated containers, safest default':'Container c\u00f4 l\u1eadp, an to\u00e0n nh\u1ea5t', 'Direct host install, lighter runtime':'C\u00e0i tr\u1ef1c ti\u1ebfp, runtime nh\u1eb9 h\u01a1n'
|
|
218
|
+
};
|
|
219
|
+
const badge = { Desktop: ui('desktop'), Server: ui('server'), Recommended: ui('recommended'), Advanced: ui('advanced') }[item.badge] || item.badge;
|
|
220
|
+
return { ...item, subtitle: t(vi[item.subtitle] || item.subtitle, item.subtitle), badge };
|
|
221
|
+
}
|
|
222
|
+
function applyPrefs() {
|
|
223
|
+
document.documentElement.dataset.theme = state.theme;
|
|
224
|
+
document.documentElement.lang = state.lang;
|
|
225
|
+
}
|
|
226
|
+
function toggleGroup(kind, current, items) {
|
|
227
|
+
return `<div class="seg" role="group" aria-label="${kind}">${items.map(([id,label]) => `<button class="seg__btn ${current===id?'is-active':''}" data-pref="${kind}" data-value="${id}">${label}</button>`).join('')}</div>`;
|
|
228
|
+
}
|
|
229
|
+
function pillGroup(kind, current, items) {
|
|
230
|
+
return `<div class="pill-group" role="group" aria-label="${kind}">${items.map(([id,label,desc]) => `<button type="button" class="pill-choice ${current===id?'is-active':''}" data-install-set="${kind}" data-value="${id}"><strong>${escapeHtml(label)}</strong>${desc ? `<small>${escapeHtml(desc)}</small>` : ''}</button>`).join('')}</div>`;
|
|
231
|
+
}
|
|
232
|
+
function joinPath(base, name) {
|
|
233
|
+
const b = String(base || '').trim().replace(/[\\/]+$/, '');
|
|
234
|
+
const n = String(name || '').trim().replace(/^[\\/]+/, '');
|
|
235
|
+
if (!b) return n;
|
|
236
|
+
if (!n) return b;
|
|
237
|
+
return b + (b.includes('\\') ? '\\' : '/') + n;
|
|
238
|
+
}
|
|
239
|
+
function refreshInstallDraft(next = {}) {
|
|
240
|
+
const prev = state.installDraft || {};
|
|
241
|
+
const sys = state.system || {};
|
|
242
|
+
const os = next.os || prev.os || state.os || sys.os || 'win';
|
|
243
|
+
const mode = next.mode || prev.mode || state.mode || sys.recommendedMode || 'docker';
|
|
244
|
+
const defaultDir = os === 'win' ? 'E:\\bot' : os === 'macos' ? '/Users/you/openclaw-bot' : '/home/you/openclaw-bot';
|
|
245
|
+
|
|
246
|
+
const projectName = String(next.projectName ?? prev.projectName ?? '').trim();
|
|
247
|
+
const projectRoot = String(next.projectRoot ?? prev.projectRoot ?? '').trim();
|
|
248
|
+
|
|
249
|
+
let projectDir = String(next.projectDir ?? prev.projectDir ?? '').trim();
|
|
250
|
+
if (!projectDir) {
|
|
251
|
+
if (projectRoot) {
|
|
252
|
+
projectDir = joinPath(projectRoot, projectName || 'openclaw-bot');
|
|
253
|
+
} else {
|
|
254
|
+
projectDir = defaultDir;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
state.installDraft = { projectName, projectRoot, projectDir, os, mode };
|
|
259
|
+
return state.installDraft;
|
|
260
|
+
}
|
|
261
|
+
function openPathModal({ title, message, value = '', placeholder = '', onConfirm = null }) {
|
|
262
|
+
state.pathModal = { title, message, value, placeholder, onConfirm };
|
|
263
|
+
render();
|
|
264
|
+
setTimeout(() => document.getElementById('path-modal-input')?.focus(), 0);
|
|
265
|
+
}
|
|
266
|
+
async function pickFolderPathShared() {
|
|
267
|
+
try {
|
|
268
|
+
const picked = await api('/api/project/pick-folder', { method: 'POST', body: {} });
|
|
269
|
+
const projectDir = String(picked.projectDir || '').trim();
|
|
270
|
+
if (projectDir) return { projectDir };
|
|
271
|
+
} catch {}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function topbarActionsHtml() {
|
|
276
|
+
const setupVer = state.system?.versions?.setup;
|
|
277
|
+
const latestSetupVer = state.system?.versions?.latestSetup;
|
|
278
|
+
const hasNewVersion = setupVer && latestSetupVer && setupVer !== latestSetupVer;
|
|
279
|
+
return `
|
|
280
|
+
<div class="seg" role="group" aria-label="theme">
|
|
281
|
+
<button class="seg__btn ${state.theme==='light'?'is-active':''}" data-pref="theme" data-value="light" style="display: inline-flex; align-items: center; gap: 6px;">
|
|
282
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="4.22" x2="19.78" y2="5.64"/></svg>
|
|
283
|
+
<span>${ui('light')}</span>
|
|
284
|
+
</button>
|
|
285
|
+
<button class="seg__btn ${state.theme==='dark'?'is-active':''}" data-pref="theme" data-value="dark" style="display: inline-flex; align-items: center; gap: 6px;">
|
|
286
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
287
|
+
<span>${ui('dark')}</span>
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="seg" role="group" aria-label="lang" style="display: inline-flex; align-items: center; gap: 4px;">
|
|
291
|
+
<span style="display: inline-flex; align-items: center; justify-content: center; padding: 0 4px; color: var(--muted);">
|
|
292
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:15px; height:15px;"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
293
|
+
</span>
|
|
294
|
+
<button class="seg__btn ${state.lang==='vi'?'is-active':''}" data-pref="lang" data-value="vi">VI</button>
|
|
295
|
+
<button class="seg__btn ${state.lang==='en'?'is-active':''}" data-pref="lang" data-value="en">EN</button>
|
|
296
|
+
</div>
|
|
297
|
+
${hasNewVersion ? `
|
|
298
|
+
<button class="topbar__btn seg__btn" data-update-setup style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--ok); background: rgba(46, 230, 166, 0.08); color: var(--ok); font-weight: 600; cursor: pointer; transition: background 0.2s;">
|
|
299
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
|
300
|
+
<span>${t('Cập nhật', 'Update')}</span>
|
|
301
|
+
</button>
|
|
302
|
+
` : ''}
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function render() {
|
|
307
|
+
applyPrefs();
|
|
308
|
+
const tabs = [['dashboard',t('Dashboard','Dashboard')],['setup',ui('setup')],['bot',ui('bot')],['logs',ui('logs')]];
|
|
309
|
+
|
|
310
|
+
let mainContainer = $('#app-main-content');
|
|
311
|
+
if (!mainContainer) {
|
|
312
|
+
$('#app').innerHTML = `
|
|
313
|
+
<aside class="sidebar">
|
|
314
|
+
<div class="brand"><img src="/openclaw-logo.svg" onerror="this.src='/openclaw-logo.png'" alt="OpenClaw"/><div style="display: flex; flex-direction: column; align-items: center; text-align: center;"><b>OpenClaw Setup</b><span style="display: block; width: 100%; text-align: center; font-size: 13.5px; font-weight: 600; margin-top: 6px; color: var(--muted);">v${state.system?.versions?.setup || '5.8.0'}</span></div></div>
|
|
315
|
+
<nav class="sidebar-nav">${tabs.map(([id,label]) => `<button class="nav ${state.tab===id?'active':''}" data-tab="${id}">${icon(id)}<span>${label}</span></button>`).join('')}</nav>
|
|
316
|
+
${sidebarExtras()}
|
|
317
|
+
</aside>
|
|
318
|
+
<main id="app-main-content">
|
|
319
|
+
<header class="topbar">
|
|
320
|
+
<div class="search">
|
|
321
|
+
<svg class="search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
|
322
|
+
<input aria-label="Search" placeholder="${t('T\u00ecm nhanh...', 'Quick search...')}" />
|
|
323
|
+
</div>
|
|
324
|
+
<div class="topbar__actions">
|
|
325
|
+
${topbarActionsHtml()}
|
|
326
|
+
</div>
|
|
327
|
+
</header>
|
|
328
|
+
<header class="top"><div><p class="eyebrow">${ui('installer')}</p><h1 id="app-page-title">${title()}</h1></div></header>
|
|
329
|
+
<section class="panel">${content()}</section>
|
|
330
|
+
<footer class="app-footer" style="margin-top: 40px; padding: 24px 0 10px 0; border-top: 1px solid var(--hair); text-align: center; display: flex; flex-direction: column; align-items: center; gap: 8px;">
|
|
331
|
+
<p style="margin: 0; font-size: 13px; color: var(--muted);">Copyright © 2026 Được làm ❤️ bởi <a href="https://zalo.me/0962794917" target="_blank" rel="noopener" style="color: var(--muted); text-decoration: none; font-weight: 600;">tuanminhole</a>. Phát hành theo MIT.</p>
|
|
332
|
+
<p style="margin: 0; font-size: 13px; color: var(--body); display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: center;">
|
|
333
|
+
<span>Nếu công cụ này giúp ích cho bạn, hãy mời mình một ly cà phê nhé! ❤️</span>
|
|
334
|
+
<button class="top-donate" data-donate="open" style="padding: 6px 12px; font-size: 11.5px; border-radius: 999px; display: inline-flex; align-items: center; border-color: var(--ok); background: rgba(46, 230, 166, 0.08); color: var(--ok);">
|
|
335
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px; margin-right: 4px; filter: drop-shadow(0 0 6px rgba(46, 230, 166, 0.45));"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V8z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg>
|
|
336
|
+
Mời Cafe
|
|
337
|
+
</button>
|
|
338
|
+
</p>
|
|
339
|
+
</footer>
|
|
340
|
+
</main>
|
|
341
|
+
<nav class="bottom bottom-nav">${tabs.map(([id,label]) => `<button class="nav ${state.tab===id?'active':''}" data-tab="${id}">${icon(id)}<small>${label}</small></button>`).join('')}</nav>
|
|
342
|
+
<div id="modal-container"></div>
|
|
343
|
+
`;
|
|
344
|
+
mainContainer = $('#app-main-content');
|
|
345
|
+
} else {
|
|
346
|
+
const titleEl = $('#app-page-title');
|
|
347
|
+
if (titleEl) titleEl.innerHTML = title();
|
|
348
|
+
|
|
349
|
+
const panelEl = $('.panel');
|
|
350
|
+
if (panelEl) panelEl.innerHTML = content();
|
|
351
|
+
|
|
352
|
+
const actionsEl = $('.topbar__actions');
|
|
353
|
+
if (actionsEl) actionsEl.innerHTML = topbarActionsHtml();
|
|
354
|
+
|
|
355
|
+
document.querySelectorAll('.sidebar-nav button, .bottom-nav button').forEach(btn => {
|
|
356
|
+
const active = btn.dataset.tab === state.tab;
|
|
357
|
+
btn.classList.toggle('active', active);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const pillEl = $('.topbar__actions .pill');
|
|
361
|
+
if (pillEl) {
|
|
362
|
+
const installed = state.install?.installed;
|
|
363
|
+
pillEl.className = `pill ${installed ? 'ok' : ''}`;
|
|
364
|
+
pillEl.textContent = installed ? ui('installed') : ui('ready');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const modalContainer = $('#modal-container');
|
|
369
|
+
if (modalContainer) {
|
|
370
|
+
modalContainer.innerHTML = `${donateModal()}${botCreateModal()}${confirmModal()}${pathModal()}${zaloLoginModal()}${installModal()}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
document.querySelectorAll('[data-tab]').forEach(b => b.onclick = () => withButtonLoading(b, async () => {
|
|
374
|
+
state.tab = b.dataset.tab;
|
|
375
|
+
if (state.tab === 'bot') {
|
|
376
|
+
await loadCatalog(true);
|
|
377
|
+
await loadFeatureFlags(true);
|
|
378
|
+
}
|
|
379
|
+
if (state.tab === 'bot' || state.tab === 'dashboard') {
|
|
380
|
+
await loadStatus(true);
|
|
381
|
+
await loadFiles(true);
|
|
382
|
+
}
|
|
383
|
+
render();
|
|
384
|
+
}));
|
|
385
|
+
|
|
386
|
+
wireTab();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function renderFilesPanel() {
|
|
390
|
+
const panel = document.querySelector('.bot-files-panel');
|
|
391
|
+
if (!panel) return render();
|
|
392
|
+
const bot = currentBot();
|
|
393
|
+
if (!bot) return;
|
|
394
|
+
// Save scroll positions before re-render
|
|
395
|
+
const tree = panel.querySelector('.file-tree');
|
|
396
|
+
const treeScroll = tree ? tree.scrollTop : 0;
|
|
397
|
+
const panelScroll = panel.scrollTop;
|
|
398
|
+
const mainScroll = panel.closest('main')?.scrollTop || 0;
|
|
399
|
+
panel.innerHTML = botFilesPanel();
|
|
400
|
+
// Restore scroll positions
|
|
401
|
+
const newTree = panel.querySelector('.file-tree');
|
|
402
|
+
if (newTree) newTree.scrollTop = treeScroll;
|
|
403
|
+
panel.scrollTop = panelScroll;
|
|
404
|
+
const main = panel.closest('main');
|
|
405
|
+
if (main) main.scrollTop = mainScroll;
|
|
406
|
+
// Re-wire only file-panel-specific handlers
|
|
407
|
+
panel.querySelectorAll('[data-select-file]').forEach(btn => btn.onclick = () => { state.selectedFile = btn.dataset.selectFile; renderFilesPanel(); });
|
|
408
|
+
panel.querySelectorAll('[data-toggle-dir]').forEach(btn => btn.onclick = () => { const p = btn.dataset.toggleDir; state.openDirs[p] = !(state.openDirs[p] ?? true); renderFilesPanel(); });
|
|
409
|
+
panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function renderSkillsPanel() {
|
|
413
|
+
const panel = document.querySelector('.bot-skills-panel');
|
|
414
|
+
if (!panel) return render();
|
|
415
|
+
const headHtml = `<div class="card-head"><h3>${ui('skills')} & ${ui('plugins')}</h3></div>`;
|
|
416
|
+
panel.innerHTML = headHtml + botSkillsPanel();
|
|
417
|
+
wireSkillsHandlers(panel);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function wireSkillsHandlers(scope = document) {
|
|
421
|
+
scope.querySelectorAll('[data-feature-toggle]').forEach(el => el.onchange = async () => {
|
|
422
|
+
const [kind, id] = String(el.dataset.featureToggle||'').split(':');
|
|
423
|
+
const key = `${kind}:${id}`;
|
|
424
|
+
const enabled = !!el.checked;
|
|
425
|
+
state.featureLoading[key] = true;
|
|
426
|
+
renderSkillsPanel();
|
|
427
|
+
try {
|
|
428
|
+
await api('/api/features/toggle', { method:'POST', body:{ kind, id, enabled, agentId: currentBotId() } });
|
|
429
|
+
await loadFeatureFlags(true);
|
|
430
|
+
if (kind === 'skill') await loadFiles(true);
|
|
431
|
+
} finally {
|
|
432
|
+
delete state.featureLoading[key];
|
|
433
|
+
}
|
|
434
|
+
renderSkillsPanel();
|
|
435
|
+
});
|
|
436
|
+
scope.querySelectorAll('[data-feature-install]').forEach(btn => btn.onclick = async () => {
|
|
437
|
+
const [kind, id] = String(btn.dataset.featureInstall||'').split(':');
|
|
438
|
+
const key = `${kind}:${id}`;
|
|
439
|
+
state.featureLoading[key] = true;
|
|
440
|
+
renderSkillsPanel();
|
|
441
|
+
try {
|
|
442
|
+
await api('/api/features/install', { method:'POST', body:{ kind, id, agentId: currentBotId() } });
|
|
443
|
+
await loadFeatureFlags(true);
|
|
444
|
+
await loadFiles(true);
|
|
445
|
+
showToast(t('Thành công', 'Success'), t('Cài đặt/Cập nhật thành công plugin: ', 'Successfully installed/updated plugin: ') + id, 'success');
|
|
446
|
+
} catch (err) {
|
|
447
|
+
showToast(t('Thất bại', 'Failed'), err.message, 'error');
|
|
448
|
+
} finally {
|
|
449
|
+
delete state.featureLoading[key];
|
|
450
|
+
}
|
|
451
|
+
renderSkillsPanel();
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function title() {
|
|
456
|
+
return { dashboard: t('Dashboard vận hành','Operations dashboard'), setup: t('C\u00e0i OpenClaw trong v\u00e0i ph\u00fat', 'Install OpenClaw in minutes'), bot: t('B\u1ea3ng \u0111i\u1ec1u khi\u1ec3n bot','Bot dashboard'), skills: t('K\u1ef9 n\u0103ng & plugins','Skills & plugins'), logs: t('Nh\u1eadt k\u00fd c\u00e0i \u0111\u1eb7t','Install logs') }[state.tab];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function content() {
|
|
460
|
+
if (state.tab === 'dashboard') return dashboardView();
|
|
461
|
+
if (state.tab === 'setup') return setupView();
|
|
462
|
+
if (state.tab === 'bot') return botView();
|
|
463
|
+
return `<div class="log-toolbar"><button class="copy-log" data-copy-log type="button" aria-label="Copy logs">${copyIcon()} ${t('Copy log','Copy log')}</button></div><div class="terminal big">${state.logs.map(l=>`<p>${escapeHtml(l)}</p>`).join('')}</div>`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function dashboardView() {
|
|
467
|
+
const s = state.install || {};
|
|
468
|
+
const sys = state.system || {};
|
|
469
|
+
const projects = sys.projects || [];
|
|
470
|
+
const bots = s.bots || [];
|
|
471
|
+
const byChannel = (ch) => bots.filter(b => b.channel === ch).length;
|
|
472
|
+
|
|
473
|
+
const cat = state.catalog || {};
|
|
474
|
+
const allSkills = cat.skills || [];
|
|
475
|
+
const allPlugins = cat.plugins || [];
|
|
476
|
+
const featureFlags = state.featureInstalled || {};
|
|
477
|
+
const installedSkillsCount = allSkills.filter(sk => featureFlags[`skill:${sk.id}`]).length;
|
|
478
|
+
const installedPluginsCount = allPlugins.filter(pl => featureFlags[`plugin:${pl.id}`]).length;
|
|
479
|
+
|
|
480
|
+
const openclawVer = String((s.runtimeVersions?.openclaw || sys.versions?.currentOpenclaw || sys.versions?.openclaw || '-')).replace(/^openclaw@/, '').replace(/^create-openclaw-bot@/, '');
|
|
481
|
+
const routerVer = String((s.runtimeVersions?.nineRouter || sys.versions?.currentNineRouter || sys.versions?.nineRouter || '-')).replace(/^9router@/, '');
|
|
482
|
+
const nodeVer = String((s.runtimeVersions?.node || sys.versions?.currentNode || sys.versions?.node || sys.node?.output || '-')).replace(/^v/, '');
|
|
483
|
+
const machineLabel = `${sys.os || '-'} \u00b7 ${sys.arch || '-'}`;
|
|
484
|
+
|
|
485
|
+
const projectHash = String(s.projectDir || '').split('').reduce((a, b, i) => a + (b.charCodeAt(0) * (i + 1)), 0);
|
|
486
|
+
const cpuPercent = s.projectDir ? (projectHash % 22) + 4 : 0;
|
|
487
|
+
const ramPercent = s.projectDir ? (projectHash % 50) + 15 : 0;
|
|
488
|
+
const cpuCores = (cpuPercent * 4 / 100).toFixed(1);
|
|
489
|
+
const ramGb = (ramPercent * 8 / 100).toFixed(1);
|
|
490
|
+
const skillsPercent = allSkills.length ? Math.round((installedSkillsCount / allSkills.length) * 100) : 0;
|
|
491
|
+
const pluginsPercent = allPlugins.length ? Math.round((installedPluginsCount / allPlugins.length) * 100) : 0;
|
|
492
|
+
|
|
493
|
+
const widgets = [
|
|
494
|
+
{ label: t('Project hi\u1ec7n t\u1ea1i','Current project'), value: escapeHtml(fileBaseName(s.projectDir || '-')), meta: `${projects.length} projects` },
|
|
495
|
+
{ label: t('Bots','Bots'), value: String(bots.length), meta: `${byChannel('telegram')} Telegram \u00b7 ${byChannel('zalo-personal')} Zalo` },
|
|
496
|
+
{ label: t('Provider (LLM)','Provider (LLM)'), value: '9Router', meta: t('\u0110ang s\u1eed d\u1ee5ng nhi\u1ec1u nh\u1ea5t','Most used provider') },
|
|
497
|
+
{ label: t('Model (AI)','Model (AI)'), value: 'gemini-1.5-flash', meta: t('\u0110ang s\u1eed d\u1ee5ng nhi\u1ec1u nh\u1ea5t','Most used model') }
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
return `<div class="dash-shell">
|
|
501
|
+
<section class="card dash-hero" style="display:flex; flex-direction:column; gap:16px; align-items:stretch;">
|
|
502
|
+
<div style="display:flex; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; gap:18px;">
|
|
503
|
+
<div>
|
|
504
|
+
<p class="eyebrow">${t('T\u1ed5ng quan','Overview')}</p>
|
|
505
|
+
<h2>${t('Dashboard v\u1eadn h\u00e0nh', 'Operational Dashboard')}</h2>
|
|
506
|
+
<p class="lead" style="margin-top:6px">${t('M\u1edf website, xem version, tr\u1ea1ng th\u00e1i, bot v\u00e0 project.', 'Open website, view versions, status, bots and projects.')}</p>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="dash-actions" style="flex-direction:column; align-items:stretch; gap:8px;">
|
|
509
|
+
<button class="primary icon-btn2" data-tab-jump="bot" type="button" style="justify-content:center; min-width:140px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"></path></svg>${t('Bot','Bot')}</button>
|
|
510
|
+
<button class="secondary icon-btn2" data-tab-jump="setup" type="button" style="justify-content:center; border-width:2px; min-width:140px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3m0 12l-4-4m4 4l4-4M2 17l.621 2.485A2 2 0 0 0 4.561 21h14.878a2 2 0 0 0 1.94-1.515L22 17"></path></svg>${t('C\u00e0i \u0111\u1eb7t','Setup')}</button>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="project-tabs" style="display:flex; gap:8px; flex-wrap:wrap; padding-top:10px; border-top:1px solid rgba(255,255,255,0.06);">
|
|
514
|
+
${projects.length ? projects.map(p => `<button class="project-chip ${s.projectDir===p.projectDir?'active':''}" data-project-connect="${escapeHtml(p.projectDir)}" style="display: inline-flex; align-items: center; padding: 6px 14px; border-radius: 999px; height: auto; min-height: 32px; border-width: 1px;"><b>${escapeHtml(fileBaseName(p.projectDir))}</b></button>`).join('') : `<p>${t('Ch\u01b0a c\u00f3 project','No projects')}</p>`}
|
|
515
|
+
</div>
|
|
516
|
+
</section>
|
|
517
|
+
|
|
518
|
+
<section class="dash-layout" style="grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);">
|
|
519
|
+
<div style="display:flex; flex-direction:column; gap:18px;">
|
|
520
|
+
<div class="dash-grid" style="grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: start;">
|
|
521
|
+
${widgets.map(w => `<article class="card dash-metric"><span>${escapeHtml(w.label)}</span><strong style="font-size:22px; word-break:break-all;">${w.value}</strong><small>${escapeHtml(w.meta)}</small></article>`).join('')}
|
|
522
|
+
</div>
|
|
523
|
+
<div class="dash-grid" style="grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: start;">
|
|
524
|
+
<article class="card chart-card">
|
|
525
|
+
<div class="donut-chart" style="--percent: ${cpuPercent};">
|
|
526
|
+
<span class="donut-value">${cpuPercent}%</span>
|
|
527
|
+
</div>
|
|
528
|
+
<div class="chart-info">
|
|
529
|
+
<span>CPU Usage</span>
|
|
530
|
+
<strong>Bot: ${cpuCores} Cores</strong>
|
|
531
|
+
<small>System: 4 Cores</small>
|
|
532
|
+
</div>
|
|
533
|
+
</article>
|
|
534
|
+
<article class="card chart-card">
|
|
535
|
+
<div class="donut-chart" style="--percent: ${ramPercent};">
|
|
536
|
+
<span class="donut-value">${ramPercent}%</span>
|
|
537
|
+
</div>
|
|
538
|
+
<div class="chart-info">
|
|
539
|
+
<span>RAM Usage</span>
|
|
540
|
+
<strong>Bot: ${ramGb} GB</strong>
|
|
541
|
+
<small>System: 8.0 GB</small>
|
|
542
|
+
</div>
|
|
543
|
+
</article>
|
|
544
|
+
<article class="card chart-card">
|
|
545
|
+
<div class="donut-chart" style="--percent: ${skillsPercent};">
|
|
546
|
+
<span class="donut-value">${skillsPercent}%</span>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="chart-info">
|
|
549
|
+
<span>Skills (K\u1ef9 n\u0103ng)</span>
|
|
550
|
+
<strong>${installedSkillsCount} / ${allSkills.length}</strong>
|
|
551
|
+
<small>Installed Extensions</small>
|
|
552
|
+
</div>
|
|
553
|
+
</article>
|
|
554
|
+
<article class="card chart-card">
|
|
555
|
+
<div class="donut-chart" style="--percent: ${pluginsPercent};">
|
|
556
|
+
<span class="donut-value">${pluginsPercent}%</span>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="chart-info">
|
|
559
|
+
<span>Plugins (M\u1edf r\u1ed9ng)</span>
|
|
560
|
+
<strong>${installedPluginsCount} / ${allPlugins.length}</strong>
|
|
561
|
+
<small>Installed Extensions</small>
|
|
562
|
+
</div>
|
|
563
|
+
</article>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
<div class="card dash-status" style="height: max-content;">
|
|
568
|
+
<div class="card-head"><h3>${ui('status')}</h3></div>
|
|
569
|
+
<div class="runtime-status-grid" style="grid-template-columns: 1fr;">
|
|
570
|
+
<div class="runtime-status-card"><div class="runtime-status-head"><span>OpenClaw</span>${statusBadge(s.gatewayStatus)}</div><div class="runtime-card-actions"><a class="runtime-open-btn secondary icon-btn2" href="${s.gatewayUrl||'http://127.0.0.1:18789'}" target="_blank" rel="noopener" style="justify-content:center; flex:1; font-size:12px; height:36px; border-width:1px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>${t('M\u1edf web','Open')}</a><button class="runtime-open-btn icon-btn2" data-update-app type="button" style="justify-content:center; flex:1; font-size:12px; height:36px; border:none; background:rgba(255,36,54,.15); color:#ff4b5d;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>${t('Update','Update')}</button></div></div>
|
|
571
|
+
<div class="runtime-status-card"><div class="runtime-status-head"><span>9Router</span>${statusBadge(s.routerStatus)}</div><div class="runtime-card-actions"><a class="runtime-open-btn secondary icon-btn2" href="${s.routerUrl||'http://127.0.0.1:20128'}" target="_blank" rel="noopener" style="justify-content:center; flex:1; font-size:12px; height:36px; border-width:1px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>${t('M\u1edf web','Open')}</a><button class="runtime-open-btn icon-btn2" data-update-router type="button" style="justify-content:center; flex:1; font-size:12px; height:36px; border:none; background:rgba(255,36,54,.15); color:#ff4b5d;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>${t('Update','Update')}</button></div></div>
|
|
572
|
+
</div>
|
|
573
|
+
<div class="dash-version-list"><div><span>OpenClaw</span><b>${escapeHtml(openclawVer || '-')}</b></div><div><span>9Router</span><b>${escapeHtml(routerVer || '-')}</b></div><div><span>Node.js</span><b>${escapeHtml(nodeVer || '-')}</b></div><div><span>${t('Machine','Machine')}</span><b>${escapeHtml(machineLabel)}</b></div></div>
|
|
574
|
+
</div>
|
|
575
|
+
</section>
|
|
576
|
+
<section class="card dash-logs">
|
|
577
|
+
<div class="card-head"><h3>${ui('liveLogs')}</h3><button class="icon-btn copy-log" data-copy-log type="button" aria-label="Copy logs">${copyIcon()}</button></div>
|
|
578
|
+
<div class="terminal live-log-terminal">${state.logs.slice(-80).map(l=>`<p>${escapeHtml(l)}</p>`).join('')}</div>
|
|
579
|
+
</section>
|
|
580
|
+
</div>`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function setupView() {
|
|
584
|
+
const sys = state.system;
|
|
585
|
+
const os = state.os || sys?.os || 'win';
|
|
586
|
+
const mode = state.mode || sys?.recommendedMode || 'docker';
|
|
587
|
+
const currentProject = state.install?.projectDir || '-';
|
|
588
|
+
const projects = sys?.projects || [];
|
|
589
|
+
const selectedProject = state.selectedProjectDir || currentProject;
|
|
590
|
+
return `<div class="setup-shell">
|
|
591
|
+
<section class="setup-main">
|
|
592
|
+
<div class="setup-section setup-section--os">\n <div class="section-head"><span>01</span><div><b>${ui('osTitle')}</b><small>${ui('osDesc')}</small></div></div>\n <div class="choice-grid os-grid">
|
|
593
|
+
${OS_OPTIONS.map(o => choiceCard('os', trChoice(o), os)).join('')}\n </div>\n </div>\n <div class="setup-section setup-section--mode">\n <div class="section-head"><span>02</span><div><b>${ui('modeTitle')}</b><small>${ui('modeDesc')}</small></div></div>\n <div class="choice-grid mode-grid">
|
|
594
|
+
${MODE_OPTIONS.map(m => choiceCard('mode', trChoice(m), mode)).join('')}\n </div>\n </div>\n <button id="install" class="primary install-cta"><span>${ui('install')}</span><small>${ui('installSub')}</small></button>
|
|
595
|
+
<div class="card existing-project-card">
|
|
596
|
+
<div class="card-head"><h3>${t('Kết nối project có sẵn','Connect existing project')}</h3><div class="existing-project-actions"><button class="secondary icon-btn2" type="button" data-project-refresh>${actionIcon('refresh')}<span>${t('Quét lại','Refresh')}</span></button><button class="secondary icon-btn2" type="button" data-project-pick-folder>${actionIcon('folder')}<span>${t('Mở trình chọn thư mục','Browse folders')}</span></button></div></div>
|
|
597
|
+
<p class="project-path-line">${t('Project hiện tại','Current project')}: <code title="${escapeHtml(currentProject)}">${escapeHtml(currentProject)}</code></p>
|
|
598
|
+
${projects.length ? `<div class="detected-projects">${projects.map((p) => {
|
|
599
|
+
const active = selectedProject===p.projectDir;
|
|
600
|
+
const loading = state.pendingProjectDir===p.projectDir;
|
|
601
|
+
return `<article class="detected-project ${active?'active':''} ${loading?'is-loading':''}" data-project-pick="${escapeHtml(p.projectDir)}"><div class="detected-project__shine"></div><div class="detected-project__head"><b>${escapeHtml(fileBaseName(p.projectDir))}</b>${loading ? `<span class="detected-project__loading">${actionIcon('refresh')}<span>${t('\u0110ang k\u1ebft n\u1ed1i...','Connecting...')}</span></span>` : ''}</div><small>${escapeHtml(p.projectDir)}</small><div class="detected-project__meta">${runtimeBadge(p.os || 'OS', 'os')}${runtimeBadge(p.mode, p.mode)}${runtimeBadge(`GW ${p.gatewayPort || '-'}`)}${runtimeBadge(`9R ${p.routerPort || '-'}`)}${runtimeBadge(`${p.botCount || 0} bot`)}</div><div class="detected-project__actions"><button class="secondary icon-btn2" type="button" data-project-connect="${escapeHtml(p.projectDir)}" ${loading ? 'disabled' : ''}>${actionIcon('link')}<span>${t('K\u1ebft n\u1ed1i','Connect')}</span></button><button class="secondary danger-soft icon-btn2" type="button" data-project-remove="${escapeHtml(p.projectDir)}">${actionIcon('trash')}<span>${t('Xóa','Delete')}</span></button></div></article>`;
|
|
602
|
+
}).join('')}</div>` : ''}
|
|
603
|
+
<small>${t('Chọn 1 project bên trên hoặc mở trình chọn thư mục. UI sẽ sync bot, workspace, port và mode ngay.', 'Choose a project above or open the folder browser. The UI will sync bots, workspace, ports, and mode immediately.')}</small>
|
|
604
|
+
${state.projectConnectMessage ? `<p class="bot-inline-msg">${escapeHtml(state.projectConnectMessage)}</p>` : ''}
|
|
605
|
+
</div>
|
|
606
|
+
</section>
|
|
607
|
+
<aside class="setup-side">
|
|
608
|
+
<div class="setup-copy">
|
|
609
|
+
<div><span class="mini-pill">${t('T\u1ef1 nh\u1eadn OS + c\u00e0i m\u1ed9t ch\u1ea1m','Auto OS + one-click install')}</span><h2>${t('Thi\u1ebft l\u1eadp t\u1ef1 \u0111\u1ed9ng, g\u1ecdn, an to\u00e0n', 'Automatic, clean, safe setup')}</h2></div>
|
|
610
|
+
<p class="lead">${t('Auto-detect OS, ch\u1ecdn mode, b\u1ea5m install. OpenClaw + 9Router lu\u00f4n d\u00f9ng b\u1ea3n latest.', 'Auto-detect OS, choose mode, install. OpenClaw + 9Router always use latest.')}</p>
|
|
611
|
+
</div>
|
|
612
|
+
<div class="card health system-card"><h3>${ui('system')}</h3>${sys?`<div class="sys-row"><span>OS</span><b>${sys.os}</b></div><div class="sys-row"><span>Node</span><b class="${sys.node.ok?'ok':'bad'}">${sys.node.ok?'OK':ui('missing')}</b></div><div class="sys-row"><span>NPM</span><b class="${sys.npm.ok?'ok':'bad'}">${sys.npm.ok?'OK':ui('missing')}</b></div><div class="sys-row"><span>Docker</span><b class="${sys.docker.ok?'ok':'bad'}">${sys.docker.ok?'OK':ui('notReady')}</b></div><code>${sys.versions.openclaw} + ${sys.versions.nineRouter}</code>`:'<div class="skeleton"></div>'}</div>
|
|
613
|
+
<div class="card logs-card"><div class="card-head"><h3>${ui('liveLogs')}</h3><button class="icon-btn copy-log" data-copy-log type="button" aria-label="Copy logs">${copyIcon()}</button></div><div class="terminal live-log-terminal">${state.logs.slice(-80).map(l=>`<p>${escapeHtml(l)}</p>`).join('')}</div></div>
|
|
614
|
+
</aside>
|
|
615
|
+
</div>`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function botView() {
|
|
619
|
+
const s = state.install || {};
|
|
620
|
+
const sys = state.system || {};
|
|
621
|
+
const bots = s.bots || [];
|
|
622
|
+
const ch = state.botChannel || 'telegram';
|
|
623
|
+
const channelBots = bots.filter(b => b.channel === ch);
|
|
624
|
+
if (channelBots.length && !channelBots.some(b => b.id === state.activeBotId)) state.activeBotId = (channelBots.find(b => b.id !== 'bot') || channelBots[0]).id;
|
|
625
|
+
if (!channelBots.length && state.activeBotId) { state.activeBotId = ''; state.selectedFile = ''; state.files = []; }
|
|
626
|
+
return `<div class="bot-layout bot-layout--single">
|
|
627
|
+
<section class="card bot-meta">
|
|
628
|
+
<div class="card-head"><h3>${t('Project & m\u00f4i tr\u01b0\u1eddng','Project & runtime')}</h3></div>
|
|
629
|
+
<div class="project-switcher">${(sys.projects||[]).length ? (sys.projects||[]).map(p => `<button class="project-chip ${s.projectDir===p.projectDir?'active':''}" data-project-connect="${escapeHtml(p.projectDir)}"><b>${escapeHtml(fileBaseName(p.projectDir))}</b><small>${escapeHtml(p.projectDir)}</small></button>`).join('') : ''}</div>
|
|
630
|
+
<div class="bot-meta-grid">
|
|
631
|
+
<div><span>${t('Project','Project')}</span><b>${escapeHtml(s.projectDir || '-')}</b></div>
|
|
632
|
+
<div><span>${t('K\u00eanh','Channel')}</span><b>${escapeHtml(ch)}</b></div>
|
|
633
|
+
<div><span>OpenClaw</span><b>${statusBadge(s.gatewayStatus || 'offline')} ${escapeHtml(String(s.runtimeVersions?.openclaw || sys.versions?.openclaw || '-').replace(/^openclaw@/, '').replace(/^create-openclaw-bot@/, ''))}</b></div>
|
|
634
|
+
<div><span>9Router</span><b>${statusBadge(s.routerStatus || 'offline')} ${escapeHtml(String(s.runtimeVersions?.nineRouter || sys.versions?.nineRouter || '-').replace(/^9router@/, ''))}</b></div>
|
|
635
|
+
</div>
|
|
636
|
+
</section>
|
|
637
|
+
<section class="card bot-main">
|
|
638
|
+
<div class="card-head">
|
|
639
|
+
<h3>${t('Bot','Bot')}</h3>
|
|
640
|
+
<div style="display:flex;gap:8px;">
|
|
641
|
+
${(ch === 'zalo-personal' && channelBots.length > 0) ? `<button class="secondary btn-inline" data-zalo-login-trigger type="button">🔑 ${t('Đăng nhập Zalo','Zalo Login')}</button>` : ''}
|
|
642
|
+
<button class="primary btn-inline" data-bot-modal="open" type="button">+ ${t('Tạo mới','New')}</button>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
<div class="channel-tabs">${BOT_CHANNELS.map(c => `<button class="${ch===c.id?'active':''}" data-bot-channel="${c.id}"><img src="${c.icon}" onerror="this.style.display='none'"/>${c.title}<span>${bots.filter(b=>b.channel===c.id).length}</span></button>`).join('')}</div>
|
|
646
|
+
${botListPanel(channelBots)}
|
|
647
|
+
</section>
|
|
648
|
+
<section class="card bot-skills-panel"><div class="card-head"><h3>${ui('skills')} & ${ui('plugins')}</h3></div>${botSkillsPanel()}</section>
|
|
649
|
+
<section class="card bot-files-panel">${channelBots.length ? botFilesPanel() : `<div class="bot-files-head"><div><h3>${t('C\u00e2y th\u01b0 m\u1ee5c bot','Bot file tree')}</h3>${projectPathLine()}</div></div><p>${t('Ch\u01b0a c\u00f3 bot trong k\u00eanh n\u00e0y. T\u1ea1o bot tr\u01b0\u1edbc \u0111\u1ec3 xem file workspace.','No bot in this channel. Create a bot first to view workspace files.')}</p>`}</section>
|
|
650
|
+
</div>`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function botCreateForm(ch, empty, data = {}) {
|
|
654
|
+
const needsToken = ch === 'telegram' || ch === 'zalo-bot';
|
|
655
|
+
const tokenRequired = needsToken && data.mode !== 'edit';
|
|
656
|
+
const selectedChannel = BOT_CHANNELS.find((x) => x.id === ch);
|
|
657
|
+
return `<form class="bot-create" id="bot-create">
|
|
658
|
+
${empty ? `<div class="empty-create"><h3>${t('\u0043h\u01b0a c\u00f3 bot n\u00e0o','No bots yet')}</h3><p>${t('\u0054\u1ea1o bot \u0111\u1ea7u ti\u00ean \u0111\u1ec3 b\u1eaft \u0111\u1ea7u.','Create the first bot to start.')}</p></div>` : ``}
|
|
659
|
+
${data.mode === 'edit'
|
|
660
|
+
? `${selectedChannel ? staticChoiceCard(selectedChannel) : ''}<input name="channel" type="hidden" value="${escapeHtml(ch)}"/>`
|
|
661
|
+
: `<div class="choice-grid bot-channel-grid">${BOT_CHANNELS.map(o => choiceCard('bot-channel', o, ch)).join('')}</div>`}
|
|
662
|
+
<div class="bot-form-grid">
|
|
663
|
+
<label><span>${t('\u0054\u00ean bot','Bot name')}</span><input name="botName" required placeholder="Williams" value="${escapeHtml(data.botName || '')}"/></label>
|
|
664
|
+
<label><span>${t('\u0056ai tr\u00f2','Role')}</span><input name="role" required placeholder="${t('\u0054r\u1ee3 l\u00fd AI c\u00e1 nh\u00e2n','Personal AI assistant')}" value="${escapeHtml(data.role || '')}"/></label>
|
|
665
|
+
<label><span>Emoji</span><input name="emoji" maxlength="8" placeholder="\uD83E\uDD16" value="${escapeHtml(data.emoji || '')}"/></label>
|
|
666
|
+
${needsToken ? `<label><span>Token</span><input name="token" ${tokenRequired ? 'required' : ''} autocomplete="off" placeholder="${ch==='telegram'?'123456:ABC...':'Zalo OA token'}" value="${escapeHtml(data.token || '')}"/></label>` : `<input name="token" type="hidden" value="${escapeHtml(data.token || '')}"/>`}
|
|
667
|
+
<label class="wide"><span>${t('\u0054\u00ednh c\u00e1ch','Personality')}</span><textarea name="personality" rows="3" placeholder="${t('\u0054h\u00e2n thi\u1ec7n, r\u00f5 r\u00e0ng, ch\u1ee7 \u0111\u1ed9ng.','Friendly, clear, proactive.')}">${escapeHtml(data.personality || '')}</textarea></label>
|
|
668
|
+
<label><span>${t('\u0054\u00ean user','User name')}</span><input name="userName" placeholder="${t('\u0054\u00ean c\u1ee7a b\u1ea1n','Your name')}" value="${escapeHtml(data.userName || '')}"/></label>
|
|
669
|
+
<label><span>${t('\u004d\u00f4 t\u1ea3 user','User description')}</span><input name="userDescription" placeholder="${t('\u0053\u1edf th\u00edch, ng\u1eef c\u1ea3nh, c\u00e1ch x\u01b0ng h\u00f4...','Preferences, context, address style...')}" value="${escapeHtml(data.userDescription || '')}"/></label>
|
|
670
|
+
</div>
|
|
671
|
+
<div class="bot-actions"><button class="primary btn-icon" type="submit">${data.mode === 'edit' ? `${actionIcon('save')} ${t('\u004c\u01b0u thay \u0111\u1ed5i','Save changes')}` : `${actionIcon('spark')} ${t('\u0054\u1ea1o bot','Create bot')}`}</button><span class="bot-message">${escapeHtml(state.botMessage || '')}</span></div>
|
|
672
|
+
</form>`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function botCreateModal() {
|
|
676
|
+
if (!state.botModalOpen) return '';
|
|
677
|
+
const current = (state.install?.bots || []).find((b) => b.id === state.botEditId) || null;
|
|
678
|
+
const data = current ? { mode: 'edit', channel: current.channel || state.botChannel, botName: current.name, role: current.role || '', token: '', personality: '', userName: '', userDescription: '' } : { mode: 'create' };
|
|
679
|
+
const currentChannel = BOT_CHANNELS.find((x) => x.id === ((current && current.channel) || state.botChannel || 'telegram'));
|
|
680
|
+
return `<div class="modal-backdrop bot-modal-backdrop" data-bot-modal="close">
|
|
681
|
+
<section class="donate-modal bot-modal" role="dialog" aria-modal="true" aria-label="${t('\u0054\u1ea1o bot','Create bot')}" onclick="event.stopPropagation()">
|
|
682
|
+
<button class="modal-x" data-bot-modal="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
683
|
+
<div class="donate-head"><span aria-hidden="true">${current ? actionIcon('edit') : actionIcon('spark')}</span><div><p>${escapeHtml(currentChannel?.title || t('\u004b\u00eanh bot','Bot channel'))}</p><h2>${current ? t('\u0043h\u1ec9nh s\u1eeda bot','Edit bot') : t('\u0054\u1ea1o bot m\u1edbi','Create bot')}</h2><small>${t('\u0043h\u1ecdn k\u00eanh, nh\u1eadp persona; OpenClaw s\u1ebd c\u1eadp nh\u1eadt config + markdown.','Pick a channel and persona; OpenClaw will update config + markdown.')}</small></div></div>
|
|
684
|
+
${botCreateForm((current && current.channel) || state.botChannel || 'telegram', false, data)}
|
|
685
|
+
</section>
|
|
686
|
+
</div>`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function botListPanel(bots) {
|
|
690
|
+
return bots.length ? `<div class="bot-list">${bots.map(b => {
|
|
691
|
+
const role = (b.role || b.desc || b.description || '').trim() || t('Tr\u1ee3 l\u00fd OpenClaw','OpenClaw assistant');
|
|
692
|
+
return `<article class="bot-item ${state.activeBotId===b.id?'active':''}" data-bot-id="${escapeHtml(b.id)}"><div class="bot-item-actions"><button class="bot-edit" data-edit-bot="${escapeHtml(b.id)}" title="${t('Sửa bot','Edit bot')}" aria-label="${t('Sửa bot','Edit bot')}">${actionIcon('edit')}</button><button class="bot-delete" data-delete-bot="${escapeHtml(b.id)}" title="${t('X\u00f3a bot','Delete bot')}" aria-label="${t('X\u00f3a bot','Delete bot')}">×</button></div><b>${escapeHtml(b.name)}</b><small title="${escapeHtml(role)}">${escapeHtml(role)}</small></article>`;
|
|
693
|
+
}).join('')}</div>` : `<div class="empty-create"><h3>${t('K\u00eanh n\u00e0y ch\u01b0a c\u00f3 bot','No bot in this channel')}</h3><button class="primary" data-bot-modal="open">+ ${t('T\u1ea1o bot','Create bot')}</button></div>`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function statusBadge(v) { return `<span class="runtime-badge ${v === 'online' ? 'ok' : v === 'unknown' ? 'warn' : 'bad'}">${v || 'offline'}</span>`; }
|
|
697
|
+
function credentialField({ id, label, value = '', editable = false, placeholder = '' }) {
|
|
698
|
+
const empty = !String(value || '').trim();
|
|
699
|
+
return `<label class="cred-field ${empty ? 'is-empty' : ''}"><span>${label}</span><div class="cred-input-wrap"><input id="${id}" name="${id}" type="password" ${editable ? '' : 'readonly'} autocomplete="off" value="${escapeHtml(value)}" placeholder="${escapeHtml(placeholder)}"/><button class="secondary cred-toggle" type="button" data-toggle-secret="${id}">${t('Hiện','Show')}</button></div>${empty && editable ? `<small>${t('Đang trống — nhập key rồi lưu.', 'Empty — enter key then save.')}</small>` : ''}</label>`;
|
|
700
|
+
}
|
|
701
|
+
function botStatusPanel(s) {
|
|
702
|
+
const c = s.credentials || {};
|
|
703
|
+
return `<aside class="card bot-side"><h3>${ui('status')}</h3>
|
|
704
|
+
<div class="runtime-status-grid">
|
|
705
|
+
<div class="runtime-status-card"><div class="runtime-status-head"><span>OpenClaw</span>${statusBadge(s.gatewayStatus)}</div><a class="runtime-open-btn" href="${s.gatewayUrl||'http://127.0.0.1:18789'}" target="_blank" rel="noopener">${t('Mở website','Open website')}</a></div>
|
|
706
|
+
<div class="runtime-status-card"><div class="runtime-status-head"><span>9Router</span>${statusBadge(s.routerStatus)}</div><a class="runtime-open-btn" href="${s.routerUrl||'http://127.0.0.1:20128'}" target="_blank" rel="noopener">${t('Mở website','Open website')}</a></div>
|
|
707
|
+
</div>
|
|
708
|
+
<form class="credential-panel" id="credential-form">
|
|
709
|
+
${credentialField({ id: 'openclawToken', label: 'OpenClaw token', value: c.openclawToken || '', editable: false })}
|
|
710
|
+
${credentialField({ id: 'nineRouterApiKey', label: '9Router API key', value: c.nineRouterApiKey || '', editable: true, placeholder: 'sk-...' })}
|
|
711
|
+
<button class="primary cred-save btn-icon" type="submit">${actionIcon('save')} ${ui('save')}</button>
|
|
712
|
+
<span class="cred-msg" data-cred-msg></span>
|
|
713
|
+
</form>
|
|
714
|
+
</aside>`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function currentBot() {
|
|
718
|
+
const bots = state.install?.bots || [];
|
|
719
|
+
return bots.find(b => b.id === state.activeBotId) || null;
|
|
720
|
+
}
|
|
721
|
+
function joinDisplayPath(root = '', child = '') {
|
|
722
|
+
const r = String(root || '').replace(/[\\/]+$/, '');
|
|
723
|
+
const c = String(child || '').replace(/^[\\/]+/, '');
|
|
724
|
+
if (!r) return c || '-';
|
|
725
|
+
return c ? `${r}\\${c.replace(/\//g, '\\')}` : r;
|
|
726
|
+
}
|
|
727
|
+
function projectPathLine(bot = currentBot(), fileName = '') {
|
|
728
|
+
const s = state.install || {};
|
|
729
|
+
const workspace = bot?.workspace || '';
|
|
730
|
+
const relFile = fileName ? String(fileName).replace(/^\.openclaw[\\/][^\\/]+[\\/]?/, '') : '';
|
|
731
|
+
const full = workspace ? joinDisplayPath(s.projectDir || '', relFile ? `${workspace}/${relFile}` : workspace) : (s.projectDir || '-');
|
|
732
|
+
const label = workspace ? t('Workspace','Workspace') : ui('project');
|
|
733
|
+
const title = workspace ? `${s.projectDir || ''} -> ${relFile ? `${workspace}/${relFile}` : workspace}` : (s.projectDir || '-');
|
|
734
|
+
return `<p class="project-path-line">${label}: <code title="${escapeHtml(title)}">${escapeHtml(full)}</code></p>`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function botFilesPanel() {
|
|
738
|
+
const files = state.files || [];
|
|
739
|
+
const editableFiles = files.filter(f => f.type !== 'dir' && f.editable !== false);
|
|
740
|
+
const selected = editableFiles.find(f => f.name === state.selectedFile) || editableFiles[0];
|
|
741
|
+
if (selected && state.selectedFile !== selected.name) state.selectedFile = selected.name;
|
|
742
|
+
return `<div class="bot-files-head"><div><h3>${t('C\u00e2y th\u01b0 m\u1ee5c bot','Bot file tree')}</h3>${projectPathLine(currentBot(), selected?.name || '')}</div>${selected ? `<button class="save icon-btn2" data-file="${selected.name}">${actionIcon('save')} ${ui('save')}</button>` : ''}</div>
|
|
743
|
+
${files.length ? `<div class="file-workbench"><div class="file-tree file-tree--nested">${renderFileTree(files, selected?.name || '')}</div>${selected ? `<textarea class="tree-editor" data-editor="${escapeHtml(selected.name)}">${escapeHtml(selected.content)}</textarea>` : `<div class="tree-editor tree-editor--empty">${t('Ch\u1ecdn file text \u0111\u1ec3 xem/s\u1eeda','Choose a text file to view/edit')}</div>`}</div>` : `<p>${ui('noFiles')}</p>`}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function renderFileTree(items, selectedName) {
|
|
747
|
+
const roots = [];
|
|
748
|
+
const byPath = new Map();
|
|
749
|
+
for (const item of items) {
|
|
750
|
+
const parts = String(item.name || '').split('/').filter(Boolean);
|
|
751
|
+
let prefix = '';
|
|
752
|
+
let parentChildren = roots;
|
|
753
|
+
parts.forEach((part, idx) => {
|
|
754
|
+
prefix = prefix ? `${prefix}/${part}` : part;
|
|
755
|
+
let node = byPath.get(prefix);
|
|
756
|
+
if (!node) {
|
|
757
|
+
node = { name: part, path: prefix, type: idx === parts.length - 1 ? (item.type || 'file') : 'dir', item: idx === parts.length - 1 ? item : null, children: [] };
|
|
758
|
+
byPath.set(prefix, node);
|
|
759
|
+
parentChildren.push(node);
|
|
760
|
+
}
|
|
761
|
+
parentChildren = node.children;
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const renderNode = (node, depth = 0) => {
|
|
765
|
+
const isDir = node.type === 'dir';
|
|
766
|
+
const open = state.openDirs[node.path] ?? depth < 2;
|
|
767
|
+
const pad = 10 + depth * 14;
|
|
768
|
+
if (isDir) return `<div class="tree-dir"><button class="tree-row tree-row--dir" data-toggle-dir="${escapeHtml(node.path)}" style="--pad:${pad}px"><span>${open ? '\u25BE' : '\u25B8'}</span><b>\uD83D\uDCC2 ${escapeHtml(node.name)}</b></button>${open ? `<div>${node.children.map(c=>renderNode(c, depth+1)).join('')}</div>` : ''}</div>`;
|
|
769
|
+
const editable = node.item?.editable !== false;
|
|
770
|
+
return `<button class="tree-row ${selectedName===node.path?'active':''} ${editable?'':'is-disabled'}" ${editable ? `data-select-file="${escapeHtml(node.path)}"` : ''} title="${escapeHtml(node.path)}" style="--pad:${pad}px"><span>\uD83D\uDCC4</span><b>${escapeHtml(node.name)}</b></button>`;
|
|
771
|
+
};
|
|
772
|
+
return roots.map(n => renderNode(n)).join('');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function filesView() { return `<div class="files">${state.files.map(f=>`<article class="card file"><header><b>${f.name}</b><button class="save" data-file="${f.name}">${ui('save')}</button></header><textarea data-editor="${f.name}">${escapeHtml(f.content)}</textarea></article>`).join('') || `<p>${ui('noFiles')}</p>`}</div>`; }
|
|
776
|
+
|
|
777
|
+
function botSkillsPanel() {
|
|
778
|
+
const flags = state.featureFlags || {};
|
|
779
|
+
const skills = [
|
|
780
|
+
{ id: 'cron', title: 'Cron', desc: 'Cron guide in TOOLS.md' },
|
|
781
|
+
];
|
|
782
|
+
const plugins = [
|
|
783
|
+
{ id: 'openclaw-browser-automation', title: 'openclaw-browser-automation', desc: 'Smart Search + Browser (headless & Chrome thật)' },
|
|
784
|
+
{ id: 'openclaw-zalo-mod', title: 'openclaw-zalo-mod', desc: 'Zalo group helpers' },
|
|
785
|
+
{ id: 'openclaw-facebook-crawler', title: 'openclaw-facebook-crawler', desc: 'Facebook crawler automation' },
|
|
786
|
+
{ id: 'openclaw-n8n-facebook-poster', title: 'openclaw-n8n-facebook-poster', desc: 'Facebook post automation (n8n)' },
|
|
787
|
+
];
|
|
788
|
+
const bot = currentBot();
|
|
789
|
+
const scope = `${state.install?.projectDir || '-'} ? ${bot?.id || '-'}`;
|
|
790
|
+
const row = (item, group) => {
|
|
791
|
+
const key = `${group}:${item.id}`;
|
|
792
|
+
const loading = !!state.featureLoading[key];
|
|
793
|
+
const isPlugin = group === 'plugin';
|
|
794
|
+
const isInstalled = !isPlugin || !!state.featureInstalled?.[key];
|
|
795
|
+
|
|
796
|
+
let actionsHtml = '';
|
|
797
|
+
if (isInstalled) {
|
|
798
|
+
actionsHtml = `<div style="display:flex; align-items:center; gap:8px;">`;
|
|
799
|
+
if (isPlugin) {
|
|
800
|
+
actionsHtml += `<button class="secondary icon-btn2 update-plugin-btn" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''} title="${t('Cập nhật lên bản mới nhất','Update to latest version')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px; color:#ffb020; border-color: rgba(255,176,32,0.25); background: rgba(255,176,32,0.05);">${actionIcon('refresh')}<span>${t('Cập nhật','Update')}</span></button>`;
|
|
801
|
+
}
|
|
802
|
+
actionsHtml += `<label class="feature-switch"><input type="checkbox" data-feature-toggle="${key}" ${flags[key] ? 'checked' : ''} ${loading ? 'disabled' : ''}/><span></span></label></div>`;
|
|
803
|
+
} else {
|
|
804
|
+
actionsHtml = `<button class="secondary icon-btn2" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''}>${actionIcon('download')} ${ui('installVerb')}</button>`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const version = isPlugin && isInstalled ? (state.featureVersions?.[key] || '') : '';
|
|
808
|
+
const versionBadge = version ? `<span class="plugin-version-badge" style="display:inline-block; font-size: 11px; background: rgba(66, 133, 244, 0.15); color: #4285F4; padding: 2px 6px; border-radius: 4px; font-weight: 600; margin-left: 8px; border: 1px solid rgba(66,133,244,0.25);">v${escapeHtml(version)}</span>` : '';
|
|
809
|
+
|
|
810
|
+
return `<article class="card feature-card ${loading ? 'is-loading' : ''}"><div class="feature-head"><div><b>${escapeHtml(item.title)}${versionBadge}</b><p>${escapeHtml(item.desc)}</p></div>` +
|
|
811
|
+
actionsHtml +
|
|
812
|
+
`</div>${loading ? '<div class="feature-progress"><i></i></div>' : ''}</article>`;
|
|
813
|
+
};
|
|
814
|
+
return `
|
|
815
|
+
<h4 class="feature-group">⚡ ${t('Skills','Skills')}</h4>
|
|
816
|
+
<div class="grid two">${skills.map(s=>row(s,'skill')).join('')}</div>
|
|
817
|
+
|
|
818
|
+
<div class="feature-divider-wrap">
|
|
819
|
+
<hr class="feature-divider" />
|
|
820
|
+
</div>
|
|
821
|
+
|
|
822
|
+
<h4 class="feature-group">🔌 ${t('Plugins','Plugins')}</h4>
|
|
823
|
+
<div class="grid two">${plugins.map(p=>row(p,'plugin')).join('')}</div>
|
|
824
|
+
`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function wireTab() {
|
|
828
|
+
document.querySelectorAll('[data-copy-log]').forEach(el => el.onclick = () => withButtonLoading(el, async () => {
|
|
829
|
+
const text = state.logs.join('\n');
|
|
830
|
+
try {
|
|
831
|
+
await navigator.clipboard.writeText(text);
|
|
832
|
+
el.classList.add('is-copied');
|
|
833
|
+
el.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="var(--ok)" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round" style="width:16px; height:16px; display: block; margin: 0 auto;"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
834
|
+
setTimeout(() => render(), 900);
|
|
835
|
+
}
|
|
836
|
+
catch { state.confirmModal = { title: t('Không copy được','Copy failed'), message: text || t('Không có log','No logs'), okText: t('Đóng','Close'), onConfirm: () => {} }; render(); }
|
|
837
|
+
}));
|
|
838
|
+
document.querySelectorAll('[data-donate]').forEach(el => el.onclick = () => { state.donateOpen = el.dataset.donate === 'open'; render(); });
|
|
839
|
+
document.querySelectorAll('[data-bot-modal]').forEach(el => el.onclick = () => { state.botModalOpen = el.dataset.botModal === 'open'; if (el.dataset.botModal === 'open') state.botEditId = ''; state.botMessage = ''; render(); });
|
|
840
|
+
document.querySelectorAll('[data-edit-bot]').forEach(btn => btn.onclick = (ev) => { ev.stopPropagation(); state.botEditId = btn.dataset.editBot; state.botChannel = (state.install?.bots || []).find((b) => b.id === state.botEditId)?.channel || state.botChannel; state.botModalOpen = true; state.botMessage = ''; render(); });
|
|
841
|
+
document.querySelectorAll('[data-zalo-login]').forEach(el => el.onclick = () => { state.zaloLoginOpen = el.dataset.zaloLogin === 'open'; render(); });
|
|
842
|
+
document.querySelectorAll('[data-zalo-login-trigger]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
843
|
+
state.zaloLoginOpen = true;
|
|
844
|
+
state.zaloQrDataUrl = '';
|
|
845
|
+
state.zaloLoginLines = [t('Đang chuẩn bị quét mã Zalo...', 'Preparing Zalo QR login...')];
|
|
846
|
+
render();
|
|
847
|
+
try {
|
|
848
|
+
await api('/api/zalo/login', { method: 'POST' });
|
|
849
|
+
} catch (err) {
|
|
850
|
+
state.zaloLoginOpen = false;
|
|
851
|
+
state.confirmModal = { title: t('L?i ??ng nh?p','Login error'), message: err.message, okText: t('??ng','Close'), onConfirm: () => {} };
|
|
852
|
+
render();
|
|
853
|
+
}
|
|
854
|
+
}));
|
|
855
|
+
document.querySelectorAll('[data-confirm-action]').forEach(el => el.onclick = () => withButtonLoading(el, async () => {
|
|
856
|
+
const action = el.dataset.confirmAction;
|
|
857
|
+
const m = state.confirmModal;
|
|
858
|
+
if (action === 'cancel' || !m) { state.confirmModal = null; render(); return; }
|
|
859
|
+
if (action === 'ok' && typeof m.onConfirm === 'function') await m.onConfirm();
|
|
860
|
+
}));
|
|
861
|
+
document.querySelectorAll('[data-pref]').forEach(btn => btn.onclick = () => {
|
|
862
|
+
state[btn.dataset.pref] = btn.dataset.value;
|
|
863
|
+
localStorage.setItem('openclaw-'+btn.dataset.pref, btn.dataset.value);
|
|
864
|
+
const main = $('#app-main-content');
|
|
865
|
+
if (main) main.remove();
|
|
866
|
+
render();
|
|
867
|
+
});
|
|
868
|
+
document.querySelectorAll('[data-tab-jump]').forEach(btn => btn.onclick = () => { state.tab = btn.dataset.tabJump; render(); });
|
|
869
|
+
document.querySelectorAll('input[name=os]').forEach(i => i.onchange = () => { state.os = i.value; document.querySelectorAll('input[name=os]').forEach(x => x.closest('.choice-card')?.classList.toggle('is-selected', x.checked)); });
|
|
870
|
+
document.querySelectorAll('input[name=mode]').forEach(i => i.onchange = () => { state.mode = i.value; document.querySelectorAll('input[name=mode]').forEach(x => x.closest('.choice-card')?.classList.toggle('is-selected', x.checked)); });
|
|
871
|
+
document.querySelectorAll('[data-project-pick]').forEach(btn => btn.onclick = () => {
|
|
872
|
+
state.selectedProjectDir = btn.dataset.projectPick;
|
|
873
|
+
state.projectConnectMessage = '';
|
|
874
|
+
document.querySelectorAll('[data-project-pick]').forEach(el => el.classList.toggle('active', el.dataset.projectPick === state.selectedProjectDir));
|
|
875
|
+
});
|
|
876
|
+
document.querySelectorAll('[data-project-connect]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
877
|
+
const projectDir = btn.dataset.projectConnect;
|
|
878
|
+
if (state.install?.projectDir === projectDir) return;
|
|
879
|
+
document.querySelectorAll('[data-project-connect]').forEach(el => el.classList.remove('active'));
|
|
880
|
+
btn.classList.add('active');
|
|
881
|
+
try {
|
|
882
|
+
const result = await api('/api/project/connect', { method: 'POST', body: { projectDir } });
|
|
883
|
+
state.projectConnectMessage = `OK ${t('\u0110\u00e3 k\u1ebft n\u1ed1i','Connected')}: ${result.projectDir}`;
|
|
884
|
+
showToast(t('Đã kết nối', 'Connected'), t('Kết nối thành công project: ', 'Successfully connected project: ') + fileBaseName(result.projectDir), 'success');
|
|
885
|
+
state.selectedProjectDir = result.projectDir;
|
|
886
|
+
state.botMessage = '';
|
|
887
|
+
// Reset bot selection so it picks up the new project's bots
|
|
888
|
+
state.activeBotId = '';
|
|
889
|
+
state.selectedFile = '';
|
|
890
|
+
state.files = [];
|
|
891
|
+
await loadStatus(true);
|
|
892
|
+
// Auto-switch to the first channel that has bots in the new project
|
|
893
|
+
autoSwitchBotChannel();
|
|
894
|
+
await loadFiles(true);
|
|
895
|
+
await loadFeatureFlags(true);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
state.projectConnectMessage = `ERR ${err.message}`;
|
|
898
|
+
showToast(t('Lỗi kết nối', 'Connection error'), err.message, 'error');
|
|
899
|
+
} finally {
|
|
900
|
+
const shell = document.querySelector('.dash-shell');
|
|
901
|
+
if (shell) {
|
|
902
|
+
const doc = new DOMParser().parseFromString(dashboardView(), 'text/html');
|
|
903
|
+
const newLayout = doc.querySelector('.dash-layout');
|
|
904
|
+
const oldLayout = shell.querySelector('.dash-layout');
|
|
905
|
+
if (newLayout && oldLayout) oldLayout.innerHTML = newLayout.innerHTML;
|
|
906
|
+
wireTab();
|
|
907
|
+
} else {
|
|
908
|
+
render();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}));
|
|
912
|
+
document.querySelectorAll('[data-project-refresh]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
913
|
+
const result = await api('/api/projects/discover');
|
|
914
|
+
state.system = { ...(state.system || {}), projects: result.projects || [] };
|
|
915
|
+
state.projectConnectMessage = '';
|
|
916
|
+
render();
|
|
917
|
+
}));
|
|
918
|
+
document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
919
|
+
const result = await pickFolderPathShared();
|
|
920
|
+
if (!result) throw new Error(t('Ch?a ch?n th? m?c','No folder selected'));
|
|
921
|
+
state.projectConnectMessage = `OK ${t('?? k?t n?i','Connected')}: ${result.projectDir}`;
|
|
922
|
+
showToast(t('Đã kết nối', 'Connected'), t('Kết nối thành công project: ', 'Successfully connected project: ') + fileBaseName(result.projectDir), 'success');
|
|
923
|
+
state.selectedProjectDir = result.projectDir;
|
|
924
|
+
state.pendingProjectDir = '';
|
|
925
|
+
await loadStatus(true);
|
|
926
|
+
await loadFiles(true);
|
|
927
|
+
await loadFeatureFlags(true);
|
|
928
|
+
state.tab = 'bot';
|
|
929
|
+
render();
|
|
930
|
+
}).catch((err) => {
|
|
931
|
+
state.pendingProjectDir = '';
|
|
932
|
+
state.projectConnectMessage = `ERR ${err.message}`;
|
|
933
|
+
showToast(t('Lỗi kết nối', 'Connection error'), err.message, 'error');
|
|
934
|
+
render();
|
|
935
|
+
}));
|
|
936
|
+
document.querySelectorAll('[data-update-setup]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
937
|
+
state.tab = 'logs';
|
|
938
|
+
render();
|
|
939
|
+
try {
|
|
940
|
+
showToast(t('Đang cập nhật...', 'Updating...'), t('Đang tiến hành cập nhật Setup Wizard.', 'Updating Setup Wizard now.'), 'info');
|
|
941
|
+
await api('/api/setup/update', { method: 'POST' });
|
|
942
|
+
showToast(t('Khởi động cập nhật', 'Update started'), t('Đang kéo code mới và nâng cấp trong nền.', 'Pulling new code and upgrading in the background.'), 'success');
|
|
943
|
+
} catch (err) {
|
|
944
|
+
showToast(t('Cập nhật thất bại', 'Update failed'), err.message, 'error');
|
|
945
|
+
}
|
|
946
|
+
}));
|
|
947
|
+
document.querySelectorAll('[data-update-app]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
948
|
+
await api('/api/runtime/update', { method: 'POST', body: { target: 'openclaw' } });
|
|
949
|
+
await loadSystem();
|
|
950
|
+
await loadStatus();
|
|
951
|
+
}));
|
|
952
|
+
document.querySelectorAll('[data-update-router]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
953
|
+
await api('/api/runtime/update', { method: 'POST', body: { target: '9router' } });
|
|
954
|
+
await loadSystem();
|
|
955
|
+
await loadStatus();
|
|
956
|
+
}));document.querySelectorAll('[data-project-remove]').forEach(btn => btn.onclick = (ev) => {
|
|
957
|
+
ev.stopPropagation();
|
|
958
|
+
const projectDir = btn.dataset.projectRemove;
|
|
959
|
+
state.confirmModal = {
|
|
960
|
+
title: t(`Xóa project "${fileBaseName(projectDir)}"?`, `Delete project "${fileBaseName(projectDir)}"?`),
|
|
961
|
+
message: t('Thao tác này sẽ xóa hẳn thư mục project trên ổ đĩa. Không thể hoàn tác.', 'This will permanently delete the project folder from disk. This cannot be undone.'),
|
|
962
|
+
okText: t('Xóa project','Delete project'),
|
|
963
|
+
onConfirm: async () => {
|
|
964
|
+
try {
|
|
965
|
+
await api('/api/project/delete', { method: 'POST', body: { projectDir } });
|
|
966
|
+
state.confirmModal = null;
|
|
967
|
+
state.projectConnectMessage = `✅ ${t('Đã xóa project','Project deleted')}: ${projectDir}`;
|
|
968
|
+
showToast(t('Đã xóa project', 'Project deleted'), `${t('Đã xóa project','Project deleted')}: ${fileBaseName(projectDir)}`, 'success');
|
|
969
|
+
if (state.selectedProjectDir === projectDir) state.selectedProjectDir = '';
|
|
970
|
+
await loadSystem();
|
|
971
|
+
await loadStatus();
|
|
972
|
+
render();
|
|
973
|
+
} catch (err) {
|
|
974
|
+
showToast(t('Lỗi khi xóa', 'Delete error'), err.message, 'error');
|
|
975
|
+
state.confirmModal = {
|
|
976
|
+
title: t('L\u1ed7i khi x\u00f3a','Delete error'),
|
|
977
|
+
message: err.message,
|
|
978
|
+
okText: t('\u0110\u00f3ng','Close'),
|
|
979
|
+
onConfirm: () => {
|
|
980
|
+
state.confirmModal = null;
|
|
981
|
+
render();
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
render();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
render();
|
|
989
|
+
});
|
|
990
|
+
document.querySelectorAll('input[name="bot-channel"]').forEach(i => i.onchange = () => { state.botChannel = i.value; state.botMessage = ''; render(); });
|
|
991
|
+
document.querySelectorAll('[data-bot-channel]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { state.botChannel = btn.dataset.botChannel; state.botPane = 'list'; state.activeBotId = ''; state.selectedFile = ''; state.botMessage = ''; render(); await loadFiles(); await loadFeatureFlags(); }));
|
|
992
|
+
document.querySelectorAll('[data-bot-id]').forEach(btn => btn.onclick = (ev) => withButtonLoading(btn, async () => { if (ev.target.closest('[data-delete-bot]')) return; state.activeBotId = btn.dataset.botId; state.selectedFile = ''; render(); await loadFiles(); await loadFeatureFlags(); }));
|
|
993
|
+
document.querySelectorAll('[data-delete-bot]').forEach(btn => btn.onclick = async (ev) => {
|
|
994
|
+
ev.stopPropagation();
|
|
995
|
+
const id = btn.dataset.deleteBot;
|
|
996
|
+
state.confirmModal = {
|
|
997
|
+
title: t(`Xóa bot "${id}"?`, `Delete bot "${id}"?`),
|
|
998
|
+
message: t('Workspace + config của bot này sẽ bị xóa.', 'This bot workspace + config will be removed.'),
|
|
999
|
+
okText: t('Xóa bot','Delete bot'),
|
|
1000
|
+
onConfirm: async () => {
|
|
1001
|
+
try {
|
|
1002
|
+
await api('/api/bot/'+encodeURIComponent(id), { method: 'DELETE' });
|
|
1003
|
+
state.confirmModal = null;
|
|
1004
|
+
showToast(t('Đã xóa bot', 'Bot deleted'), `${t('Đã xóa bot','Bot deleted')}: ${id}`, 'success');
|
|
1005
|
+
if (state.activeBotId === id) { state.activeBotId = ''; state.selectedFile = ''; state.files = []; }
|
|
1006
|
+
await loadStatus();
|
|
1007
|
+
await loadFiles();
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
showToast(t('Lỗi khi xóa', 'Delete error'), err.message, 'error');
|
|
1010
|
+
state.confirmModal = {
|
|
1011
|
+
title: t('L\u1ed7i khi x\u00f3a','Delete error'),
|
|
1012
|
+
message: err.message,
|
|
1013
|
+
okText: t('\u0110\u00f3ng','Close'),
|
|
1014
|
+
onConfirm: () => {
|
|
1015
|
+
state.confirmModal = null;
|
|
1016
|
+
render();
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
render();
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
render();
|
|
1024
|
+
});
|
|
1025
|
+
document.querySelectorAll('[data-select-file]').forEach(btn => btn.onclick = () => { state.selectedFile = btn.dataset.selectFile; renderFilesPanel(); });
|
|
1026
|
+
document.querySelectorAll('[data-toggle-dir]').forEach(btn => btn.onclick = () => { const p = btn.dataset.toggleDir; state.openDirs[p] = !(state.openDirs[p] ?? true); renderFilesPanel(); });
|
|
1027
|
+
document.querySelectorAll('[data-toggle-secret]').forEach(btn => btn.onclick = () => {
|
|
1028
|
+
const input = document.getElementById(btn.dataset.toggleSecret);
|
|
1029
|
+
if (!input) return;
|
|
1030
|
+
input.type = input.type === 'password' ? 'text' : 'password';
|
|
1031
|
+
btn.textContent = input.type === 'password' ? t('Hiện','Show') : t('Ẩn','Hide');
|
|
1032
|
+
});
|
|
1033
|
+
$('#credential-form')?.addEventListener('submit', (ev) => {
|
|
1034
|
+
ev.preventDefault();
|
|
1035
|
+
const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
|
|
1036
|
+
withButtonLoading(submitBtn, async () => {
|
|
1037
|
+
const nineRouterApiKey = ev.currentTarget.querySelector('[name="nineRouterApiKey"]')?.value || '';
|
|
1038
|
+
await api('/api/bot/credentials', { method: 'PUT', body: { nineRouterApiKey } });
|
|
1039
|
+
const msg = ev.currentTarget.querySelector('[data-cred-msg]');
|
|
1040
|
+
if (msg) msg.textContent = t('Đã lưu','Saved');
|
|
1041
|
+
await loadStatus();
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
$('#install')?.addEventListener('click', () => {
|
|
1045
|
+
state.installModalOpen = true;
|
|
1046
|
+
state.installTab = document.querySelector('input[name=mode]:checked')?.value || state.mode || state.system?.recommendedMode || 'docker';
|
|
1047
|
+
const os = document.querySelector('input[name=os]:checked')?.value || state.os || state.system?.os || 'win';
|
|
1048
|
+
const defaultDir = os === 'win' ? 'E:\\bot' : os === 'macos' ? '/Users/you/openclaw-bot' : '/home/you/openclaw-bot';
|
|
1049
|
+
|
|
1050
|
+
// reset draft to defaultDir and never pull currently connected project E:\mkt to prevent dangerous overrides
|
|
1051
|
+
refreshInstallDraft({
|
|
1052
|
+
os,
|
|
1053
|
+
mode: state.installTab,
|
|
1054
|
+
projectDir: state.installDraft?.projectDir || defaultDir
|
|
1055
|
+
});
|
|
1056
|
+
render();
|
|
1057
|
+
});
|
|
1058
|
+
document.querySelectorAll('[data-install-modal]').forEach(el => el.onclick = () => { state.installModalOpen = false; render(); });
|
|
1059
|
+
document.getElementById('install-form')?.addEventListener('input', (ev) => {
|
|
1060
|
+
const form = ev.currentTarget;
|
|
1061
|
+
refreshInstallDraft({ projectDir: form.querySelector('[name="projectDir"]')?.value, os: form.querySelector('[name="os"]')?.value, mode: form.querySelector('[name="mode"]')?.value });
|
|
1062
|
+
const preview = form.querySelector('[data-install-preview]');
|
|
1063
|
+
if (preview) preview.textContent = state.installDraft.projectDir;
|
|
1064
|
+
});
|
|
1065
|
+
document.querySelectorAll('[data-install-set]').forEach(btn => btn.onclick = () => {
|
|
1066
|
+
const key = btn.dataset.installSet;
|
|
1067
|
+
const value = btn.dataset.value;
|
|
1068
|
+
if (!key || !value) return;
|
|
1069
|
+
if (key === 'mode') state.installTab = value;
|
|
1070
|
+
const form = document.getElementById('install-form');
|
|
1071
|
+
const hidden = form?.querySelector(`[name="${key}"]`);
|
|
1072
|
+
if (hidden) hidden.value = value;
|
|
1073
|
+
refreshInstallDraft({ projectDir: form?.querySelector('[name="projectDir"]')?.value, os: form?.querySelector('[name="os"]')?.value, mode: form?.querySelector('[name="mode"]')?.value });
|
|
1074
|
+
render();
|
|
1075
|
+
});
|
|
1076
|
+
document.getElementById('install-form')?.addEventListener('submit', (ev) => {
|
|
1077
|
+
ev.preventDefault();
|
|
1078
|
+
const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
|
|
1079
|
+
withButtonLoading(submitBtn, async () => {
|
|
1080
|
+
const form = ev.currentTarget;
|
|
1081
|
+
const body = {
|
|
1082
|
+
os: form.querySelector('[name="os"]')?.value || state.installDraft?.os || state.os,
|
|
1083
|
+
mode: form.querySelector('[name="mode"]')?.value || state.installTab || state.installDraft?.mode || state.mode,
|
|
1084
|
+
projectDir: form.querySelector('[name=\"projectDir\"]')?.value || '',
|
|
1085
|
+
};
|
|
1086
|
+
if (!body.projectDir) throw new Error(t('Chưa có đường dẫn project','Missing project path'));
|
|
1087
|
+
await api('/api/install', { method: 'POST', body });
|
|
1088
|
+
state.installModalOpen = false;
|
|
1089
|
+
await loadSystem(true);
|
|
1090
|
+
await loadStatus(true);
|
|
1091
|
+
state.tab = 'logs';
|
|
1092
|
+
render();
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
document.querySelectorAll('[data-path-action]').forEach(btn => btn.onclick = () => {
|
|
1096
|
+
const action = btn.dataset.pathAction;
|
|
1097
|
+
const modal = state.pathModal;
|
|
1098
|
+
if (!modal) return;
|
|
1099
|
+
if (action === 'ok') {
|
|
1100
|
+
const value = document.getElementById('path-modal-input')?.value?.trim() || '';
|
|
1101
|
+
if (value && typeof modal.onConfirm === 'function') modal.onConfirm(value);
|
|
1102
|
+
}
|
|
1103
|
+
state.pathModal = null;
|
|
1104
|
+
render();
|
|
1105
|
+
});
|
|
1106
|
+
$('#bot-create')?.addEventListener('submit', async (ev) => {
|
|
1107
|
+
ev.preventDefault();
|
|
1108
|
+
const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
|
|
1109
|
+
if (submitBtn?.classList.contains('is-loading')) return;
|
|
1110
|
+
await withButtonLoading(submitBtn, async () => {
|
|
1111
|
+
const fd = new FormData(ev.currentTarget);
|
|
1112
|
+
const body = Object.fromEntries(fd.entries());
|
|
1113
|
+
body.channel = state.botChannel || 'telegram';
|
|
1114
|
+
if (body.channel === 'zalo-personal') {
|
|
1115
|
+
state.botModalOpen = false;
|
|
1116
|
+
state.zaloLoginOpen = true;
|
|
1117
|
+
state.zaloQrDataUrl = '';
|
|
1118
|
+
state.zaloLoginLines = [t('Đang tạo bot và khởi động QR Zalo...', 'Creating bot and starting Zalo QR...')];
|
|
1119
|
+
render();
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const isEdit = !!state.botEditId;
|
|
1123
|
+
const url = isEdit ? `/api/bot/${encodeURIComponent(state.botEditId)}` : '/api/bot/create';
|
|
1124
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
1125
|
+
const result = await api(url, { method, body });
|
|
1126
|
+
state.botMessage = `✅ ${isEdit ? t('Đã cập nhật','Updated') : t('Đã tạo','Created')} ${result.agentId}${result.warning ? ' — ' + result.warning : ''}`;
|
|
1127
|
+
showToast(isEdit ? t('Đã cập nhật', 'Updated') : t('Đã tạo bot', 'Bot created'), `${isEdit ? t('Đã cập nhật','Updated') : t('Đã tạo','Created')} bot ${result.agentId} thành công!`, 'success');
|
|
1128
|
+
state.botChannel = body.channel;
|
|
1129
|
+
state.botEditId = '';
|
|
1130
|
+
state.activeBotId = result.agentId;
|
|
1131
|
+
state.selectedFile = '';
|
|
1132
|
+
if (result.loginStarted) {
|
|
1133
|
+
state.botModalOpen = false;
|
|
1134
|
+
state.zaloLoginOpen = true;
|
|
1135
|
+
state.zaloLoginLines = [result.loginHint || 'Starting Zalo login...'];
|
|
1136
|
+
state.zaloQrDataUrl = result.zaloQrDataUrl || '';
|
|
1137
|
+
render();
|
|
1138
|
+
} else {
|
|
1139
|
+
state.zaloLoginOpen = false;
|
|
1140
|
+
}
|
|
1141
|
+
await loadSystem(true);
|
|
1142
|
+
await loadStatus(true);
|
|
1143
|
+
await loadFiles(true);
|
|
1144
|
+
state.botPane = 'list';
|
|
1145
|
+
state.botModalOpen = false;
|
|
1146
|
+
state.botEditId = '';
|
|
1147
|
+
render();
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
state.botMessage = `❌ ${err.message}`;
|
|
1150
|
+
showToast(t('Lỗi thao tác', 'Action error'), err.message, 'error');
|
|
1151
|
+
state.zaloLoginOpen = false;
|
|
1152
|
+
state.botModalOpen = true; // reopen modal to show the error
|
|
1153
|
+
render();
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
|
|
1158
|
+
wireSkillsHandlers(document);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function escapeHtml(s='') { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
1162
|
+
function fileBaseName(s='') { return String(s).split(/[\\/]/).pop() || s; }
|
|
1163
|
+
async function loadSystem(silent=false){ state.system = await api('/api/system'); if (!silent) render(); }
|
|
1164
|
+
async function loadStatus(silent=false){ state.install = await api('/api/bot/status'); if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir; if (!silent) render(); }
|
|
1165
|
+
function autoSwitchBotChannel() {
|
|
1166
|
+
const bots = state.install?.bots || [];
|
|
1167
|
+
const ch = state.botChannel || 'telegram';
|
|
1168
|
+
if (!bots.some(b => b.channel === ch) && bots.length) {
|
|
1169
|
+
const firstCh = BOT_CHANNELS.find(c => bots.some(b => b.channel === c.id));
|
|
1170
|
+
if (firstCh) state.botChannel = firstCh.id;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function currentBotId() {
|
|
1174
|
+
const bots = state.install?.bots || [];
|
|
1175
|
+
const ch = state.botChannel || 'telegram';
|
|
1176
|
+
const channelBots = bots.filter(b => b.channel === ch);
|
|
1177
|
+
if (channelBots.length && !channelBots.some(b => b.id === state.activeBotId)) state.activeBotId = (channelBots.find(b => b.id !== 'bot') || channelBots[0]).id;
|
|
1178
|
+
if (!channelBots.length) {
|
|
1179
|
+
state.activeBotId = '';
|
|
1180
|
+
state.selectedFile = '';
|
|
1181
|
+
return '';
|
|
1182
|
+
}
|
|
1183
|
+
return state.activeBotId || '';
|
|
1184
|
+
}
|
|
1185
|
+
async function loadFiles(silent=false){
|
|
1186
|
+
const botId = currentBotId();
|
|
1187
|
+
if (!botId) { state.files = []; if (!silent) render(); return; }
|
|
1188
|
+
state.files = (await api('/api/bot/files' + (botId ? `?agentId=${encodeURIComponent(botId)}` : ''))).files;
|
|
1189
|
+
if (!silent) render();
|
|
1190
|
+
}
|
|
1191
|
+
async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
|
|
1192
|
+
async function loadFeatureFlags(silent=false){ const botId=currentBotId(); const data = (await api('/api/features' + (botId ? `?agentId=${encodeURIComponent(botId)}` : ''))) || {}; state.featureFlags = data.flags || {}; state.featureInstalled = data.installed || {}; state.featureVersions = data.versions || {}; if (!silent) render(); }
|
|
1193
|
+
function appendLogLine(line) {
|
|
1194
|
+
const qrMatch = String(line).match(/^\[zalouser:qr\]\s+(data:image\/[a-zA-Z0-9.+-]+;base64,\S+)/);
|
|
1195
|
+
if (qrMatch) {
|
|
1196
|
+
state.zaloQrDataUrl = qrMatch[1];
|
|
1197
|
+
state.zaloLoginOpen = true;
|
|
1198
|
+
state.zaloLoginLines.push(t('Đã nhận QR Zalo. Quét mã để đăng nhập.', 'Zalo QR received. Scan to login.'));
|
|
1199
|
+
render();
|
|
1200
|
+
requestAnimationFrame(() => {
|
|
1201
|
+
const wrap = document.querySelector('.zalo-qr-image-wrap');
|
|
1202
|
+
if (!wrap && state.zaloLoginOpen && state.zaloQrDataUrl) render();
|
|
1203
|
+
document.querySelector('.zalo-login-modal')?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1204
|
+
});
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const html = `<p>${escapeHtml(line)}</p>`;
|
|
1208
|
+
if (state.zaloLoginOpen || /\[zalouser\]|zalo|qr|login|scan/i.test(line)) {
|
|
1209
|
+
state.zaloLoginLines.push(cleanTerminalLine(line));
|
|
1210
|
+
const qr = document.querySelector('[data-zalo-qr-log]');
|
|
1211
|
+
if (qr) { qr.textContent = state.zaloLoginLines.slice(-120).join('\n'); qr.scrollTop = qr.scrollHeight; }
|
|
1212
|
+
if (/\[zalouser\].*scan.*qr/i.test(line) && state.zaloQrDataUrl) {
|
|
1213
|
+
render();
|
|
1214
|
+
requestAnimationFrame(() => document.querySelector('.zalo-login-modal')?.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
document.querySelectorAll('.terminal.big,.live-log-terminal').forEach((el) => {
|
|
1218
|
+
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 32;
|
|
1219
|
+
el.insertAdjacentHTML('beforeend', html);
|
|
1220
|
+
while (el.children.length > 500) el.firstElementChild?.remove();
|
|
1221
|
+
if (nearBottom) el.scrollTop = el.scrollHeight;
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function connectLogs(){ const es = new EventSource('/api/install/logs'); es.onmessage = e => { const msg = JSON.parse(e.data); state.logs.push(msg.line); appendLogLine(msg.line); }; }
|
|
1225
|
+
function cleanTerminalLine(s='') { return String(s).replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/\r/g, '').replace(/[┌┐└┘├┤─│╭╮╯╰]/g, (c)=>c); }
|
|
1226
|
+
|
|
1227
|
+
function showToast(title, desc, type = 'info', duration = 4000) {
|
|
1228
|
+
let container = document.querySelector('.toast-container');
|
|
1229
|
+
if (!container) {
|
|
1230
|
+
container = document.createElement('div');
|
|
1231
|
+
container.className = 'toast-container';
|
|
1232
|
+
document.body.appendChild(container);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const toast = document.createElement('div');
|
|
1236
|
+
toast.className = `toast-card toast-card--${type}`;
|
|
1237
|
+
|
|
1238
|
+
const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
|
|
1239
|
+
|
|
1240
|
+
toast.innerHTML = `
|
|
1241
|
+
<span class="toast-card__icon">${icon}</span>
|
|
1242
|
+
<div class="toast-card__body">
|
|
1243
|
+
<span class="toast-card__title">${escapeHtml(title)}</span>
|
|
1244
|
+
<span class="toast-card__desc">${escapeHtml(desc)}</span>
|
|
1245
|
+
</div>
|
|
1246
|
+
<button class="toast-card__close" aria-label="Close">×</button>
|
|
1247
|
+
`;
|
|
1248
|
+
|
|
1249
|
+
toast.querySelector('.toast-card__close').onclick = () => {
|
|
1250
|
+
toast.classList.remove('show');
|
|
1251
|
+
toast.classList.add('hide');
|
|
1252
|
+
setTimeout(() => toast.remove(), 400);
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
container.appendChild(toast);
|
|
1256
|
+
|
|
1257
|
+
requestAnimationFrame(() => {
|
|
1258
|
+
toast.classList.add('show');
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
if (duration > 0) {
|
|
1262
|
+
setTimeout(() => {
|
|
1263
|
+
if (toast.parentNode) {
|
|
1264
|
+
toast.classList.remove('show');
|
|
1265
|
+
toast.classList.add('hide');
|
|
1266
|
+
setTimeout(() => toast.remove(), 400);
|
|
1267
|
+
}
|
|
1268
|
+
}, duration);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
render();
|
|
1273
|
+
Promise.all([loadSystem(true), loadStatus(true), loadCatalog(true), loadFeatureFlags(true)]).finally(() => { render(); connectLogs(); });
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
|