@wendongfly/myhi 1.3.188 → 1.3.191
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/bin/daemon.js +196 -170
- package/bin/install-check.js +25 -0
- package/dist/admin.html +144 -27
- package/dist/chat.html +144 -79
- package/dist/index.html +171 -16
- package/dist/index.js +4 -4
- package/dist/index.min.js +14 -18
- package/dist/lib/machine.js +11 -1
- package/dist/lib/prefs.js +51 -0
- package/dist/terminal.html +1 -1
- package/package.json +1 -1
package/dist/admin.html
CHANGED
|
@@ -41,6 +41,20 @@
|
|
|
41
41
|
.inp-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
|
|
42
42
|
.inp-row .inp { flex: 1; }
|
|
43
43
|
.btn-sm { padding: 0.5rem 1rem; background: #7c3aed; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 0.85rem; white-space: nowrap; }
|
|
44
|
+
.btn-sm.ghost { background: transparent; color: #b1bac4; border: 1px solid rgba(255,255,255,0.15); }
|
|
45
|
+
.mode-status { font-size: 0.85rem; line-height: 1.6; padding: 0.6rem 0.75rem; border-radius: 10px; background: rgba(124,58,237,0.1); border: 1px solid rgba(124,58,237,0.25); margin-bottom: 0.5rem; }
|
|
46
|
+
.mode-status b { color: #e6edf3; }
|
|
47
|
+
.mode-status small { display: block; color: #8b949e; font-size: 0.75rem; }
|
|
48
|
+
.sw { position: relative; width: 38px; height: 22px; flex-shrink: 0; }
|
|
49
|
+
.sw input { opacity: 0; width: 0; height: 0; }
|
|
50
|
+
.sw span { position: absolute; inset: 0; background: #30363d; border-radius: 11px; cursor: pointer; transition: 0.2s; }
|
|
51
|
+
.sw span::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: 0.2s; }
|
|
52
|
+
.sw input:checked + span { background: #7c3aed; }
|
|
53
|
+
.sw input:checked + span::before { transform: translateX(16px); }
|
|
54
|
+
.dir-pick { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; max-height: 220px; overflow-y: auto; margin-bottom: 0.5rem; font-size: 0.8rem; }
|
|
55
|
+
.dir-pick div { padding: 0.4rem 0.6rem; cursor: pointer; color: #b1bac4; }
|
|
56
|
+
.dir-pick div:hover { background: rgba(255,255,255,0.05); }
|
|
57
|
+
.dir-pick .cur { color: #8b949e; font-size: 0.72rem; cursor: default; border-bottom: 1px solid rgba(255,255,255,0.06); }
|
|
44
58
|
.msg { font-size: 0.8rem; margin-top: 0.3rem; min-height: 1.2em; }
|
|
45
59
|
.msg-ok { color: #3fb950; }
|
|
46
60
|
.msg-err { color: #f85149; }
|
|
@@ -101,21 +115,33 @@
|
|
|
101
115
|
</div>
|
|
102
116
|
|
|
103
117
|
<!-- 用户管理 -->
|
|
104
|
-
<div class="card">
|
|
105
|
-
<h3
|
|
118
|
+
<div class="card" id="mode">
|
|
119
|
+
<h3>使用模式</h3>
|
|
120
|
+
<div id="mode-status" class="mode-status"></div>
|
|
106
121
|
<div id="user-list"></div>
|
|
107
|
-
<div
|
|
108
|
-
<
|
|
122
|
+
<div id="excl-row" class="row" style="display:none">
|
|
123
|
+
<span>独占模式<br><small style="color:#6e7681">同一时间只允许一个人登录,别人登录会被挡住</small></span>
|
|
124
|
+
<label class="sw"><input type="checkbox" id="excl-toggle" onchange="setExclusive(this.checked)"><span></span></label>
|
|
125
|
+
</div>
|
|
126
|
+
<button class="btn-sm" id="add-open" onclick="openAddMember()" style="margin-top:0.6rem;width:100%"></button>
|
|
127
|
+
<div id="add-member" style="display:none;margin-top:0.7rem">
|
|
128
|
+
<div class="inp-row"><input class="inp" id="u-name" placeholder="成员名称(如:张三)"></div>
|
|
109
129
|
<div class="inp-row">
|
|
110
|
-
<input class="inp" id="u-pwd" placeholder="
|
|
111
|
-
<
|
|
130
|
+
<input class="inp" id="u-pwd" placeholder="登录密码(至少 4 位)" autocomplete="off">
|
|
131
|
+
<button class="btn-sm ghost" onclick="genPwd()">随机生成</button>
|
|
112
132
|
</div>
|
|
113
133
|
<div class="inp-row">
|
|
114
|
-
<input class="inp" id="u-dir" placeholder="
|
|
115
|
-
<button class="btn-sm" onclick="
|
|
134
|
+
<input class="inp" id="u-dir" placeholder="工作目录(留空 = 不限制)">
|
|
135
|
+
<button class="btn-sm ghost" onclick="pickDir()">浏览…</button>
|
|
136
|
+
</div>
|
|
137
|
+
<div id="dir-pick" style="display:none" class="dir-pick"></div>
|
|
138
|
+
<div class="inp-row" style="margin-top:0.2rem">
|
|
139
|
+
<button class="btn-sm ghost" onclick="closeAddMember()" style="flex:1">取消</button>
|
|
140
|
+
<button class="btn-sm" onclick="addUser()" style="flex:2">添加</button>
|
|
116
141
|
</div>
|
|
117
|
-
<div class="msg" id="u-msg"></div>
|
|
118
142
|
</div>
|
|
143
|
+
<div class="msg" id="u-msg"></div>
|
|
144
|
+
<div id="mode-foot" style="font-size:0.72rem;color:#6e7681;margin-top:0.5rem;line-height:1.5"></div>
|
|
119
145
|
</div>
|
|
120
146
|
|
|
121
147
|
<!-- 修改管理密码 -->
|
|
@@ -369,7 +395,7 @@ async function refresh() {
|
|
|
369
395
|
}
|
|
370
396
|
|
|
371
397
|
// 用户列表
|
|
372
|
-
renderUsers(d.users || []);
|
|
398
|
+
renderUsers(d.users || [], d.exclusive);
|
|
373
399
|
|
|
374
400
|
// 运行时长
|
|
375
401
|
if (d.uptime) {
|
|
@@ -379,20 +405,70 @@ async function refresh() {
|
|
|
379
405
|
} catch {}
|
|
380
406
|
}
|
|
381
407
|
|
|
382
|
-
|
|
408
|
+
// 使用模式:有成员 = 多用户,没有 = 单用户(默认)
|
|
409
|
+
let _users = [];
|
|
410
|
+
function renderUsers(users, exclusive) {
|
|
411
|
+
_users = users;
|
|
412
|
+
const multi = users.length > 0;
|
|
413
|
+
document.getElementById('mode-status').innerHTML = multi
|
|
414
|
+
? `<b>当前:多用户模式 · ${users.length} 个成员</b><small>成员用各自密码登录,可以升级 / 重启,不能改全局设置;你用管理密码或二维码登录,权限不变。</small>`
|
|
415
|
+
: `<b>当前:单用户模式</b><small>只有你在用,所有功能都能用。需要给别人用时再添加成员。</small>`;
|
|
416
|
+
document.getElementById('add-open').textContent = multi ? '+ 添加成员' : '+ 添加成员,开启多用户模式';
|
|
417
|
+
document.getElementById('excl-row').style.display = multi ? '' : 'none';
|
|
418
|
+
document.getElementById('excl-toggle').checked = !!exclusive;
|
|
419
|
+
document.getElementById('mode-foot').textContent = multi ? '删除全部成员即回到单用户模式。删除成员或重置密码后,他当前的登录会立即失效。' : '';
|
|
383
420
|
const el = document.getElementById('user-list');
|
|
384
|
-
if (!users.length) { el.innerHTML = '<div class="row" style="color:#6e7681">暂无用户</div>'; return; }
|
|
385
421
|
el.innerHTML = users.map(u => `
|
|
386
422
|
<div class="row">
|
|
387
|
-
<span><b style="color:#e6edf3">${esc(u.name)}</b> <span style="color:#6e7681;font-size:0.
|
|
423
|
+
<span><b style="color:#e6edf3">${esc(u.name)}</b> <span style="color:#6e7681;font-size:0.72rem">${u.dir ? '📁 ' + esc(u.dir) : '不限目录'}</span></span>
|
|
388
424
|
<div class="row-actions">
|
|
389
|
-
<button onclick="editUserPwd('${esc(u.name)}')"
|
|
425
|
+
<button onclick="editUserPwd('${esc(u.name)}')">重置密码</button>
|
|
390
426
|
<button class="del" onclick="delUser('${esc(u.name)}')">删除</button>
|
|
391
427
|
</div>
|
|
392
428
|
</div>
|
|
393
429
|
`).join('');
|
|
394
430
|
}
|
|
395
431
|
|
|
432
|
+
function openAddMember() {
|
|
433
|
+
document.getElementById('add-member').style.display = '';
|
|
434
|
+
document.getElementById('add-open').style.display = 'none';
|
|
435
|
+
document.getElementById('u-name').focus();
|
|
436
|
+
}
|
|
437
|
+
function closeAddMember() {
|
|
438
|
+
document.getElementById('add-member').style.display = 'none';
|
|
439
|
+
document.getElementById('add-open').style.display = '';
|
|
440
|
+
document.getElementById('dir-pick').style.display = 'none';
|
|
441
|
+
}
|
|
442
|
+
function genPwd() {
|
|
443
|
+
const cs = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
444
|
+
const a = crypto.getRandomValues(new Uint32Array(8));
|
|
445
|
+
document.getElementById('u-pwd').value = [...a].map(n => cs[n % cs.length]).join('');
|
|
446
|
+
}
|
|
447
|
+
// 目录点选:复用 /api/dirs(主站以管理员身份登录时可用;不行就手动输入)
|
|
448
|
+
async function pickDir(path) {
|
|
449
|
+
const box = document.getElementById('dir-pick');
|
|
450
|
+
box.style.display = '';
|
|
451
|
+
try {
|
|
452
|
+
const r = await fetch('/api/dirs' + (path ? '?path=' + encodeURIComponent(path) : ''));
|
|
453
|
+
const d = await r.json();
|
|
454
|
+
if (!r.ok) throw new Error(d.error || r.status);
|
|
455
|
+
document.getElementById('u-dir').value = d.current;
|
|
456
|
+
box.innerHTML = `<div class="cur">${esc(d.current)}</div>` +
|
|
457
|
+
(d.parent ? `<div data-p="${esc(d.parent)}">⬆ 上一级</div>` : '') +
|
|
458
|
+
(d.drives || []).map(x => `<div data-p="${esc(x)}">💽 ${esc(x)}</div>`).join('') +
|
|
459
|
+
d.dirs.map(x => `<div data-p="${esc(x.path)}">📁 ${esc(x.name)}</div>`).join('');
|
|
460
|
+
box.querySelectorAll('[data-p]').forEach(el => el.onclick = () => pickDir(el.dataset.p));
|
|
461
|
+
} catch (e) { box.innerHTML = `<div class="cur">无法浏览目录(${esc(e.message)}),请直接输入路径</div>`; }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function setExclusive(on) {
|
|
465
|
+
try {
|
|
466
|
+
const r = await fetch('/api/admin/exclusive', { method: 'POST', headers: H(), body: JSON.stringify({ on }) });
|
|
467
|
+
const d = await r.json();
|
|
468
|
+
if (!d.ok) { alert(d.error || '失败'); refresh(); }
|
|
469
|
+
} catch { alert('连接失败'); refresh(); }
|
|
470
|
+
}
|
|
471
|
+
|
|
396
472
|
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
397
473
|
|
|
398
474
|
async function addUser() {
|
|
@@ -400,17 +476,27 @@ async function addUser() {
|
|
|
400
476
|
const name = document.getElementById('u-name').value.trim();
|
|
401
477
|
const dir = document.getElementById('u-dir').value.trim();
|
|
402
478
|
const msg = document.getElementById('u-msg');
|
|
403
|
-
if (!
|
|
479
|
+
if (!name || !pwd) { msg.className = 'msg msg-err'; msg.textContent = '成员名称和登录密码必填'; return; }
|
|
480
|
+
// 第一个成员 = 从单用户切到多用户:先讲清楚会发生什么
|
|
481
|
+
if (!_users.length && !confirm(
|
|
482
|
+
'添加第一个成员后,myhi 进入「多用户模式」:\n\n' +
|
|
483
|
+
'· 成员用各自的密码登录' + (dir ? ',只能在绑定的目录里建任务' : '') + ';\n' +
|
|
484
|
+
'· 成员可以升级、重启,但不能改全局设置(本机显示名、语音、用户、网关等);\n' +
|
|
485
|
+
'· 你(管理员)照常用管理密码或二维码登录,权限不变。\n\n' +
|
|
486
|
+
'以后删除全部成员即回到单用户模式。确定添加?')) return;
|
|
404
487
|
try {
|
|
405
488
|
const r = await fetch('/api/admin/user', { method: 'POST', headers: H(), body: JSON.stringify({ password: pwd, name, dir }) });
|
|
406
489
|
const d = await r.json();
|
|
407
|
-
if (d.ok) {
|
|
408
|
-
|
|
490
|
+
if (d.ok) {
|
|
491
|
+
msg.className = 'msg msg-ok'; msg.textContent = `已添加「${name}」,登录密码:${pwd}(请告知对方)`;
|
|
492
|
+
['u-pwd', 'u-name', 'u-dir'].forEach(id => document.getElementById(id).value = '');
|
|
493
|
+
closeAddMember(); refresh();
|
|
494
|
+
} else { msg.className = 'msg msg-err'; msg.textContent = d.error || '失败'; }
|
|
409
495
|
} catch { msg.className = 'msg msg-err'; msg.textContent = '连接失败'; }
|
|
410
496
|
}
|
|
411
497
|
|
|
412
498
|
async function editUserPwd(name) {
|
|
413
|
-
const newPwd = prompt(
|
|
499
|
+
const newPwd = prompt(`为 ${name} 设置新的登录密码(至少 4 位,设置后他当前的登录会失效):`);
|
|
414
500
|
if (!newPwd) return;
|
|
415
501
|
try {
|
|
416
502
|
const r = await fetch('/api/admin/user/password', { method: 'POST', headers: H(), body: JSON.stringify({ name, password: newPwd }) });
|
|
@@ -421,7 +507,7 @@ async function editUserPwd(name) {
|
|
|
421
507
|
}
|
|
422
508
|
|
|
423
509
|
async function delUser(name) {
|
|
424
|
-
if (!confirm(
|
|
510
|
+
if (!confirm(`确定删除成员「${name}」?他当前的登录会立即失效。` + (_users.length === 1 ? '\n\n这是最后一个成员,删除后回到单用户模式。' : ''))) return;
|
|
425
511
|
try {
|
|
426
512
|
const r = await fetch('/api/admin/user', { method: 'DELETE', headers: H(), body: JSON.stringify({ name }) });
|
|
427
513
|
const d = await r.json();
|
|
@@ -442,15 +528,38 @@ async function changePwd() {
|
|
|
442
528
|
} catch { msg.className = 'msg msg-err'; msg.textContent = '连接失败'; }
|
|
443
529
|
}
|
|
444
530
|
|
|
445
|
-
|
|
531
|
+
// 等升级落地:轮询 /api/version(重启/停服期间连不上是正常的)。
|
|
532
|
+
// 成功 = 版本变了(且等于目标);失败 = 服务端报了本次升级的失败原因;10 分钟兜底超时。
|
|
533
|
+
async function waitUpgrade(from, target, onTick) {
|
|
534
|
+
const t0 = Date.now();
|
|
535
|
+
while (Date.now() - t0 < 10 * 60 * 1000) {
|
|
536
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
537
|
+
let v = null;
|
|
538
|
+
try { const r = await fetch('/api/version', { cache: 'no-store' }); if (r.ok) v = await r.json(); } catch {}
|
|
539
|
+
onTick?.(!!v, Math.round((Date.now() - t0) / 1000));
|
|
540
|
+
if (!v) continue;
|
|
541
|
+
if (v.current !== from && (!target || v.current === target)) return { ok: true, version: v.current };
|
|
542
|
+
const last = v.upgrade?.last;
|
|
543
|
+
if (!v.upgrade?.running && last && !last.ok && last.at >= t0 - 5000) return { ok: false, error: last.error };
|
|
544
|
+
}
|
|
545
|
+
return { ok: false, error: '等了 10 分钟仍未完成,请稍后刷新页面查看版本,或到服务器上查看 myhi log' };
|
|
546
|
+
}
|
|
547
|
+
async function doUpgrade(force) {
|
|
446
548
|
const btn = document.getElementById('btn-up'), msg = document.getElementById('op-msg');
|
|
447
|
-
|
|
549
|
+
const fail = (t) => { msg.textContent = '失败: ' + t; btn.disabled = false; btn.textContent = '重试'; };
|
|
550
|
+
btn.disabled = true; btn.textContent = '升级中...'; msg.textContent = '正在提交升级...'; msg.style.display = '';
|
|
448
551
|
try {
|
|
449
|
-
const r = await fetch('/api/admin/upgrade', { method: 'POST', headers: H() });
|
|
450
|
-
const d = await r.json();
|
|
451
|
-
if (d.ok) {
|
|
452
|
-
|
|
453
|
-
|
|
552
|
+
const r = await fetch('/api/admin/upgrade', { method: 'POST', headers: { ...H(), 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force }) });
|
|
553
|
+
const d = await r.json().catch(() => ({}));
|
|
554
|
+
if (!d.ok) {
|
|
555
|
+
if (d.busy?.length && confirm(d.error + '\n\n仍然现在升级吗?')) return doUpgrade(true);
|
|
556
|
+
return fail(d.error || ('HTTP ' + r.status));
|
|
557
|
+
}
|
|
558
|
+
msg.textContent = d.message || '升级中...';
|
|
559
|
+
const res = await waitUpgrade(d.from, d.target, (up, sec) => { msg.textContent = (up ? '正在安装新版本… ' : '服务重启中… ') + sec + 's'; });
|
|
560
|
+
if (res.ok) { msg.textContent = '已升级到 v' + res.version + ',即将刷新'; setTimeout(() => location.reload(), 800); }
|
|
561
|
+
else fail(res.error);
|
|
562
|
+
} catch { fail('连接失败'); }
|
|
454
563
|
}
|
|
455
564
|
|
|
456
565
|
async function doClaudeUpgrade() {
|
|
@@ -492,7 +601,15 @@ async function waitReload() {
|
|
|
492
601
|
document.getElementById('op-msg').textContent = '重启超时,请手动刷新';
|
|
493
602
|
}
|
|
494
603
|
|
|
495
|
-
|
|
604
|
+
// 主站已是管理员 → 直接换后台令牌免登录;否则沿用本标签页已有令牌(重启后会失效)或显示登录框
|
|
605
|
+
(async () => {
|
|
606
|
+
try {
|
|
607
|
+
const r = await fetch('/api/admin/sso', { method: 'POST' });
|
|
608
|
+
const d = r.ok ? await r.json() : null;
|
|
609
|
+
if (d?.ok) { T = d.token; sessionStorage.setItem('myhi_admin', T); return showAdmin(); }
|
|
610
|
+
} catch {}
|
|
611
|
+
if (T) showAdmin();
|
|
612
|
+
})();
|
|
496
613
|
</script>
|
|
497
614
|
</body>
|
|
498
615
|
</html>
|