@wendongfly/myhi 1.3.190 → 1.3.192
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/dist/admin.html +182 -67
- package/dist/chat.html +88 -65
- package/dist/grid.html +2 -1
- package/dist/index.html +403 -30
- package/dist/index.js +4 -4
- package/dist/index.min.js +66 -65
- package/dist/lib/machine.js +5 -2
- package/dist/lib/prefs.js +51 -0
- package/dist/login.html +175 -163
- package/package.json +1 -1
package/dist/admin.html
CHANGED
|
@@ -5,47 +5,73 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>myhi 管理</title>
|
|
7
7
|
<script src="/lib/machine.js"></script>
|
|
8
|
+
<script src="/theme.js"></script>
|
|
9
|
+
<script>
|
|
10
|
+
// 主题与其他页面一致:浏览器选过的优先,否则服务端默认(新装浅色、老机器深色)
|
|
11
|
+
(function(){try{var t=localStorage.getItem('myhi_theme')||window.MYHI_DEFAULT_THEME||'dark';
|
|
12
|
+
if(t==='system') t=matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';
|
|
13
|
+
if(t!=='dark') document.documentElement.setAttribute('data-theme',t);}catch(e){}})();
|
|
14
|
+
</script>
|
|
8
15
|
<style>
|
|
16
|
+
:root { --bg:#0d1117; --surface:#161b22; --text:#e6edf3; --text2:#b1bac4; --text3:#8b949e; --muted:#6e7681;
|
|
17
|
+
--line:rgba(255,255,255,0.06); --border:rgba(255,255,255,0.12); --border2:#30363d; --accent:#7c3aed; }
|
|
18
|
+
:root[data-theme="light"] { --bg:#f6f8fa; --surface:#ffffff; --text:#1f2328; --text2:#424a53; --text3:#57606a; --muted:#6e7781;
|
|
19
|
+
--line:rgba(0,0,0,0.06); --border:#d0d7de; --border2:#d0d7de; --accent:#8250df; }
|
|
20
|
+
:root[data-theme="black"] { --bg:#000000; --surface:#0a0a0a; --border2:#262626; }
|
|
9
21
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
-
body { font-family: -apple-system, system-ui, sans-serif; background:
|
|
22
|
+
body { font-family: -apple-system, system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
11
23
|
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; }
|
|
12
|
-
.login-box { background:
|
|
24
|
+
.login-box { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 2rem; width: 100%; max-width: 360px; }
|
|
13
25
|
.login-box h2 { text-align: center; margin-bottom: 1.5rem; font-size: 1.2rem; }
|
|
14
|
-
.login-box input { width: 100%; padding: 0.8rem; background:
|
|
15
|
-
.login-box button { width: 100%; padding: 0.8rem; background:
|
|
26
|
+
.login-box input { width: 100%; padding: 0.8rem; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; color: var(--text); font-size: 1rem; margin-bottom: 1rem; }
|
|
27
|
+
.login-box button { width: 100%; padding: 0.8rem; background: var(--accent); color: #fff; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
|
|
16
28
|
.login-box .err { color: #f85149; font-size: 0.85rem; text-align: center; margin-bottom: 0.5rem; min-height: 1.2em; }
|
|
17
29
|
.wrap { max-width: 640px; margin: 0 auto; padding: 1rem; }
|
|
18
|
-
.header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 0; border-bottom: 1px solid
|
|
30
|
+
.header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 0; border-bottom: 1px solid var(--line); margin-bottom: 1.5rem; }
|
|
19
31
|
.header h1 { font-size: 1.1rem; }
|
|
20
|
-
.header .logout { background: none; border: 1px solid
|
|
21
|
-
.ver-card { background:
|
|
22
|
-
.ver-big { font-size: 2rem; font-weight: 800; color:
|
|
23
|
-
.ver-sub { font-size: 0.85rem; color:
|
|
32
|
+
.header .logout { background: none; border: 1px solid var(--border); color: var(--text3); padding: 0.3rem 0.8rem; border-radius: 8px; cursor: pointer; font-size: 0.8rem; }
|
|
33
|
+
.ver-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 1.2rem 1.5rem; margin-bottom: 1.2rem; }
|
|
34
|
+
.ver-big { font-size: 2rem; font-weight: 800; color: var(--accent); }
|
|
35
|
+
.ver-sub { font-size: 0.85rem; color: var(--text3); margin-top: 0.5rem; }
|
|
24
36
|
.ver-sub b { color: #3fb950; }
|
|
25
37
|
.actions { display: flex; gap: 0.8rem; margin-bottom: 1.5rem; }
|
|
26
38
|
.actions button { flex: 1; padding: 0.8rem; border: none; border-radius: 10px; font-size: 0.9rem; font-weight: 600; cursor: pointer; }
|
|
27
|
-
.btn-up { background:
|
|
28
|
-
.btn-up:disabled { background:
|
|
39
|
+
.btn-up { background: var(--accent); color: #fff; }
|
|
40
|
+
.btn-up:disabled { background: var(--border2); color: var(--muted); cursor: not-allowed; }
|
|
29
41
|
.btn-up.glow { background: #3fb950; animation: pulse 2s infinite; }
|
|
30
|
-
.btn-re { background:
|
|
42
|
+
.btn-re { background: var(--line); color: var(--text); border: 1px solid var(--border) !important; }
|
|
31
43
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.7} }
|
|
32
|
-
.card { background:
|
|
44
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 1rem 1.2rem; margin-bottom: 1rem; }
|
|
33
45
|
.card h3 { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.8rem; }
|
|
34
|
-
.row { font-size: 0.82rem; color:
|
|
35
|
-
.row + .row { border-top: 1px solid
|
|
46
|
+
.row { font-size: 0.82rem; color: var(--text2); padding: 0.5rem 0; line-height: 1.5; display: flex; justify-content: space-between; align-items: center; }
|
|
47
|
+
.row + .row { border-top: 1px solid var(--line); }
|
|
36
48
|
.row-actions { display: flex; gap: 0.4rem; }
|
|
37
|
-
.row-actions button { background: none; border: 1px solid
|
|
38
|
-
.row-actions button:hover { color:
|
|
49
|
+
.row-actions button { background: none; border: 1px solid var(--border); color: var(--text3); padding: 0.2rem 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.75rem; }
|
|
50
|
+
.row-actions button:hover { color: var(--text); border-color: var(--border2); }
|
|
39
51
|
.row-actions .del { color: #f85149; border-color: rgba(248,81,73,0.3); }
|
|
40
|
-
.inp { padding: 0.5rem 0.7rem; background:
|
|
52
|
+
.inp { padding: 0.5rem 0.7rem; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; color: var(--text); font-size: 0.85rem; width: 100%; }
|
|
41
53
|
.inp-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
|
|
42
54
|
.inp-row .inp { flex: 1; }
|
|
43
|
-
.btn-sm { padding: 0.5rem 1rem; background:
|
|
55
|
+
.btn-sm { padding: 0.5rem 1rem; background: var(--accent); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 0.85rem; white-space: nowrap; }
|
|
56
|
+
.btn-sm.ghost { background: transparent; color: var(--text2); border: 1px solid var(--border); }
|
|
57
|
+
.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; }
|
|
58
|
+
.mode-status b { color: var(--text); }
|
|
59
|
+
.mode-status small { display: block; color: var(--text3); font-size: 0.75rem; }
|
|
60
|
+
.sw { position: relative; width: 38px; height: 22px; flex-shrink: 0; }
|
|
61
|
+
.sw input { opacity: 0; width: 0; height: 0; }
|
|
62
|
+
.sw span { position: absolute; inset: 0; background: var(--border2); border-radius: 11px; cursor: pointer; transition: 0.2s; }
|
|
63
|
+
.sw span::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: 0.2s; }
|
|
64
|
+
.sw input:checked + span { background: var(--accent); }
|
|
65
|
+
.sw input:checked + span::before { transform: translateX(16px); }
|
|
66
|
+
.dir-pick { border: 1px solid var(--border); border-radius: 8px; max-height: 220px; overflow-y: auto; margin-bottom: 0.5rem; font-size: 0.8rem; }
|
|
67
|
+
.dir-pick div { padding: 0.4rem 0.6rem; cursor: pointer; color: var(--text2); }
|
|
68
|
+
.dir-pick div:hover { background: var(--line); }
|
|
69
|
+
.dir-pick .cur { color: var(--text3); font-size: 0.72rem; cursor: default; border-bottom: 1px solid var(--line); }
|
|
44
70
|
.msg { font-size: 0.8rem; margin-top: 0.3rem; min-height: 1.2em; }
|
|
45
71
|
.msg-ok { color: #3fb950; }
|
|
46
72
|
.msg-err { color: #f85149; }
|
|
47
|
-
.status-msg { text-align: center; padding: 1rem; color:
|
|
48
|
-
.uptime { font-size: 0.78rem; color:
|
|
73
|
+
.status-msg { text-align: center; padding: 1rem; color: var(--text3); font-size: 0.85rem; }
|
|
74
|
+
.uptime { font-size: 0.78rem; color: var(--muted); text-align: center; margin-top: 1rem; }
|
|
49
75
|
</style>
|
|
50
76
|
</head>
|
|
51
77
|
<body>
|
|
@@ -67,7 +93,7 @@
|
|
|
67
93
|
|
|
68
94
|
<!-- 版本 + 升级 -->
|
|
69
95
|
<div class="ver-card">
|
|
70
|
-
<div style="font-size:0.8rem;color
|
|
96
|
+
<div style="font-size:0.8rem;color:var(--text3)">当前版本</div>
|
|
71
97
|
<div class="ver-big" id="v-cur">--</div>
|
|
72
98
|
<div class="ver-sub" id="v-latest"></div>
|
|
73
99
|
</div>
|
|
@@ -79,7 +105,7 @@
|
|
|
79
105
|
|
|
80
106
|
<!-- Claude Code 版本 + 升级 -->
|
|
81
107
|
<div class="ver-card">
|
|
82
|
-
<div style="font-size:0.8rem;color
|
|
108
|
+
<div style="font-size:0.8rem;color:var(--text3)">Claude Code 版本</div>
|
|
83
109
|
<div class="ver-big" id="cl-cur">--</div>
|
|
84
110
|
<div class="ver-sub" id="cl-latest"></div>
|
|
85
111
|
</div>
|
|
@@ -91,31 +117,43 @@
|
|
|
91
117
|
<!-- 在线用户 -->
|
|
92
118
|
<div class="card">
|
|
93
119
|
<h3>在线用户</h3>
|
|
94
|
-
<div id="online-users"><div class="row" style="color
|
|
120
|
+
<div id="online-users"><div class="row" style="color:var(--muted)">加载中...</div></div>
|
|
95
121
|
</div>
|
|
96
122
|
|
|
97
123
|
<!-- 活跃会话 -->
|
|
98
124
|
<div class="card">
|
|
99
125
|
<h3>活跃会话</h3>
|
|
100
|
-
<div id="active-sessions"><div class="row" style="color
|
|
126
|
+
<div id="active-sessions"><div class="row" style="color:var(--muted)">加载中...</div></div>
|
|
101
127
|
</div>
|
|
102
128
|
|
|
103
129
|
<!-- 用户管理 -->
|
|
104
|
-
<div class="card">
|
|
105
|
-
<h3
|
|
130
|
+
<div class="card" id="mode">
|
|
131
|
+
<h3>使用模式</h3>
|
|
132
|
+
<div id="mode-status" class="mode-status"></div>
|
|
106
133
|
<div id="user-list"></div>
|
|
107
|
-
<div
|
|
108
|
-
<
|
|
134
|
+
<div id="excl-row" class="row" style="display:none">
|
|
135
|
+
<span>独占模式<br><small style="color:var(--muted)">同一时间只允许一个人登录,别人登录会被挡住</small></span>
|
|
136
|
+
<label class="sw"><input type="checkbox" id="excl-toggle" onchange="setExclusive(this.checked)"><span></span></label>
|
|
137
|
+
</div>
|
|
138
|
+
<button class="btn-sm" id="add-open" onclick="openAddMember()" style="margin-top:0.6rem;width:100%"></button>
|
|
139
|
+
<div id="add-member" style="display:none;margin-top:0.7rem">
|
|
140
|
+
<div class="inp-row"><input class="inp" id="u-name" placeholder="成员名称(如:张三)"></div>
|
|
109
141
|
<div class="inp-row">
|
|
110
|
-
<input class="inp" id="u-pwd" placeholder="
|
|
111
|
-
<
|
|
142
|
+
<input class="inp" id="u-pwd" placeholder="登录密码(至少 4 位)" autocomplete="off">
|
|
143
|
+
<button class="btn-sm ghost" onclick="genPwd()">随机生成</button>
|
|
112
144
|
</div>
|
|
113
145
|
<div class="inp-row">
|
|
114
|
-
<input class="inp" id="u-dir" placeholder="
|
|
115
|
-
<button class="btn-sm" onclick="
|
|
146
|
+
<input class="inp" id="u-dir" placeholder="工作目录(留空 = 不限制)">
|
|
147
|
+
<button class="btn-sm ghost" onclick="pickDir()">浏览…</button>
|
|
148
|
+
</div>
|
|
149
|
+
<div id="dir-pick" style="display:none" class="dir-pick"></div>
|
|
150
|
+
<div class="inp-row" style="margin-top:0.2rem">
|
|
151
|
+
<button class="btn-sm ghost" onclick="closeAddMember()" style="flex:1">取消</button>
|
|
152
|
+
<button class="btn-sm" onclick="addUser()" style="flex:2">添加</button>
|
|
116
153
|
</div>
|
|
117
|
-
<div class="msg" id="u-msg"></div>
|
|
118
154
|
</div>
|
|
155
|
+
<div class="msg" id="u-msg"></div>
|
|
156
|
+
<div id="mode-foot" style="font-size:0.72rem;color:var(--muted);margin-top:0.5rem;line-height:1.5"></div>
|
|
119
157
|
</div>
|
|
120
158
|
|
|
121
159
|
<!-- 修改管理密码 -->
|
|
@@ -131,8 +169,8 @@
|
|
|
131
169
|
<!-- 大模型网关 -->
|
|
132
170
|
<div class="card">
|
|
133
171
|
<h3>大模型网关</h3>
|
|
134
|
-
<div style="font-size:0.75rem;color
|
|
135
|
-
配置后新建的 Claude 会话自动注入 <code style="background
|
|
172
|
+
<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.6rem;line-height:1.5">
|
|
173
|
+
配置后新建的 Claude 会话自动注入 <code style="background:var(--surface);padding:0.1em 0.3em;border-radius:3px">ANTHROPIC_BASE_URL</code> / <code style="background:var(--surface);padding:0.1em 0.3em;border-radius:3px">ANTHROPIC_API_KEY</code>
|
|
136
174
|
</div>
|
|
137
175
|
<div class="inp-row">
|
|
138
176
|
<input type="text" class="inp" id="gw-url" placeholder="网关 URL(如 https://gateway.example.com)">
|
|
@@ -147,11 +185,11 @@
|
|
|
147
185
|
<!-- 命名密钥库 -->
|
|
148
186
|
<div class="card">
|
|
149
187
|
<h3>密钥库(Secret Vault)</h3>
|
|
150
|
-
<div style="font-size:0.75rem;color
|
|
188
|
+
<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.6rem;line-height:1.5">
|
|
151
189
|
集中存 GitLab PAT / 部署 token / 网关 key / 证书等敏感值,spawn 会话时注入成环境变量,AI/命令用
|
|
152
|
-
<code style="background
|
|
190
|
+
<code style="background:var(--surface);padding:0.1em 0.3em;border-radius:3px">$名字</code>
|
|
153
191
|
引用——密钥值不进聊天、不落 transcript。类型选「文件」(证书等)时注入的是路径
|
|
154
|
-
<code style="background
|
|
192
|
+
<code style="background:var(--surface);padding:0.1em 0.3em;border-radius:3px">$MYHI_SECRET_名字</code>。
|
|
155
193
|
作用域「项目」仅注给该 cwd 的会话(最小权限)。<b>密钥值绝不写进记忆/CLAUDE.md。</b>
|
|
156
194
|
</div>
|
|
157
195
|
<div id="sec-list" style="margin-bottom:0.6rem"></div>
|
|
@@ -219,22 +257,22 @@ async function loadSecrets() {
|
|
|
219
257
|
const d = await r.json();
|
|
220
258
|
const box = document.getElementById('sec-list');
|
|
221
259
|
let html = '';
|
|
222
|
-
const row = (s, scope, cwd) => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid
|
|
223
|
-
<span style="color
|
|
224
|
-
<span style="color
|
|
225
|
-
<span style="color
|
|
260
|
+
const row = (s, scope, cwd) => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--line);font-size:0.8rem">
|
|
261
|
+
<span style="color:var(--text);font-family:monospace">${escHtml(s.name)}</span>
|
|
262
|
+
<span style="color:var(--muted)">${s.type === 'file' ? '📄文件' : '🔑变量'}</span>
|
|
263
|
+
<span style="color:var(--muted);font-family:monospace;flex:1">${escHtml(s.preview)}</span>
|
|
226
264
|
<button class="btn-sm" style="padding:0.1rem 0.5rem" onclick="delSecret('${escHtml(s.name)}','${scope}','${escHtml(cwd || '')}')">删</button>
|
|
227
265
|
</div>`;
|
|
228
266
|
if (d.global && d.global.length) {
|
|
229
|
-
html += '<div style="color
|
|
267
|
+
html += '<div style="color:var(--text3);font-size:0.72rem;margin:0.2rem 0">全局</div>';
|
|
230
268
|
html += d.global.map(s => row(s, 'global', '')).join('');
|
|
231
269
|
}
|
|
232
270
|
for (const p of (d.projects || [])) {
|
|
233
271
|
if (!p.secrets.length) continue;
|
|
234
|
-
html += `<div style="color
|
|
272
|
+
html += `<div style="color:var(--text3);font-size:0.72rem;margin:0.4rem 0 0.2rem;font-family:monospace">项目 ${escHtml(p.cwd)}</div>`;
|
|
235
273
|
html += p.secrets.map(s => row(s, 'project', p.cwd)).join('');
|
|
236
274
|
}
|
|
237
|
-
box.innerHTML = html || '<div style="color
|
|
275
|
+
box.innerHTML = html || '<div style="color:var(--muted);font-size:0.78rem">(暂无密钥)</div>';
|
|
238
276
|
} catch {}
|
|
239
277
|
}
|
|
240
278
|
|
|
@@ -330,11 +368,16 @@ async function refresh() {
|
|
|
330
368
|
const cr = await fetch('/api/claude/version', { headers: H() });
|
|
331
369
|
if (cr.ok) {
|
|
332
370
|
const cd = await cr.json();
|
|
333
|
-
document.getElementById('cl-cur').textContent = cd.current ? ('v' + cd.current) : '
|
|
371
|
+
document.getElementById('cl-cur').textContent = cd.current ? ('v' + cd.current) : '未安装';
|
|
334
372
|
const clBtn = document.getElementById('btn-cl-up');
|
|
373
|
+
_clInstalled = !!cd.current;
|
|
335
374
|
if (cd.upgrade?.running) {
|
|
336
|
-
document.getElementById('cl-latest').innerHTML = '<span style="color:#58a6ff"
|
|
337
|
-
clBtn.disabled = true; clBtn.textContent = '
|
|
375
|
+
document.getElementById('cl-latest').innerHTML = '<span style="color:#58a6ff">' + (cd.current ? '升级' : '安装') + '中...</span>';
|
|
376
|
+
clBtn.disabled = true; clBtn.textContent = (cd.current ? '升级' : '安装') + '中...';
|
|
377
|
+
} else if (!cd.current) {
|
|
378
|
+
// 没装(npm 装 myhi 不会顺带装它):别显示「已是最新」,给安装按钮
|
|
379
|
+
document.getElementById('cl-latest').innerHTML = '<span style="color:#f0883e">AI 任务需要它,这台电脑上还没有</span>' + (cd.latest ? ',最新版 v' + cd.latest : '');
|
|
380
|
+
clBtn.disabled = false; clBtn.classList.add('glow'); clBtn.textContent = '安装 Claude Code';
|
|
338
381
|
} else if (cd.updateAvailable) {
|
|
339
382
|
document.getElementById('cl-latest').innerHTML = '最新版本: <b>v' + cd.latest + '</b>';
|
|
340
383
|
clBtn.disabled = false; clBtn.classList.add('glow'); clBtn.textContent = '升级到 v' + cd.latest;
|
|
@@ -348,28 +391,28 @@ async function refresh() {
|
|
|
348
391
|
// 在线用户
|
|
349
392
|
const ouEl = document.getElementById('online-users');
|
|
350
393
|
if (!d.onlineUsers?.length) {
|
|
351
|
-
ouEl.innerHTML = '<div class="row" style="color
|
|
394
|
+
ouEl.innerHTML = '<div class="row" style="color:var(--muted)">无在线用户</div>';
|
|
352
395
|
} else {
|
|
353
396
|
ouEl.innerHTML = d.onlineUsers.map(u =>
|
|
354
|
-
`<div class="row"><span>${esc(u.name)}</span><span style="color
|
|
397
|
+
`<div class="row"><span>${esc(u.name)}</span><span style="color:var(--muted)">${esc(u.role)}</span></div>`
|
|
355
398
|
).join('');
|
|
356
399
|
}
|
|
357
400
|
|
|
358
401
|
// 活跃会话
|
|
359
402
|
const asEl = document.getElementById('active-sessions');
|
|
360
403
|
if (!d.sessions?.length) {
|
|
361
|
-
asEl.innerHTML = '<div class="row" style="color
|
|
404
|
+
asEl.innerHTML = '<div class="row" style="color:var(--muted)">无活跃会话</div>';
|
|
362
405
|
} else {
|
|
363
406
|
asEl.innerHTML = d.sessions.map(s => {
|
|
364
407
|
const tag = s.mode === 'agent'
|
|
365
408
|
? '<span style="display:inline-block;font-size:0.7rem;padding:0.1rem 0.4rem;border-radius:4px;background:rgba(124,58,237,0.2);color:#a78bfa">Agent</span>'
|
|
366
409
|
: '<span style="display:inline-block;font-size:0.7rem;padding:0.1rem 0.4rem;border-radius:4px;background:rgba(63,185,80,0.2);color:#3fb950">PTY</span>';
|
|
367
|
-
return `<div class="row"><span>${tag} ${esc(s.title)} <span style="color
|
|
410
|
+
return `<div class="row"><span>${tag} ${esc(s.title)} <span style="color:var(--muted)">${esc(s.owner||'')}</span></span><span style="color:var(--muted)">${s.viewers}人</span></div>`;
|
|
368
411
|
}).join('');
|
|
369
412
|
}
|
|
370
413
|
|
|
371
414
|
// 用户列表
|
|
372
|
-
renderUsers(d.users || []);
|
|
415
|
+
renderUsers(d.users || [], d.exclusive);
|
|
373
416
|
|
|
374
417
|
// 运行时长
|
|
375
418
|
if (d.uptime) {
|
|
@@ -379,20 +422,70 @@ async function refresh() {
|
|
|
379
422
|
} catch {}
|
|
380
423
|
}
|
|
381
424
|
|
|
382
|
-
|
|
425
|
+
// 使用模式:有成员 = 多用户,没有 = 单用户(默认)
|
|
426
|
+
let _users = [];
|
|
427
|
+
function renderUsers(users, exclusive) {
|
|
428
|
+
_users = users;
|
|
429
|
+
const multi = users.length > 0;
|
|
430
|
+
document.getElementById('mode-status').innerHTML = multi
|
|
431
|
+
? `<b>当前:多用户模式 · ${users.length} 个成员</b><small>成员用各自密码登录,可以升级 / 重启,不能改全局设置;你用管理密码或二维码登录,权限不变。</small>`
|
|
432
|
+
: `<b>当前:单用户模式</b><small>只有你在用,所有功能都能用。需要给别人用时再添加成员。</small>`;
|
|
433
|
+
document.getElementById('add-open').textContent = multi ? '+ 添加成员' : '+ 添加成员,开启多用户模式';
|
|
434
|
+
document.getElementById('excl-row').style.display = multi ? '' : 'none';
|
|
435
|
+
document.getElementById('excl-toggle').checked = !!exclusive;
|
|
436
|
+
document.getElementById('mode-foot').textContent = multi ? '删除全部成员即回到单用户模式。删除成员或重置密码后,他当前的登录会立即失效。' : '';
|
|
383
437
|
const el = document.getElementById('user-list');
|
|
384
|
-
if (!users.length) { el.innerHTML = '<div class="row" style="color:#6e7681">暂无用户</div>'; return; }
|
|
385
438
|
el.innerHTML = users.map(u => `
|
|
386
439
|
<div class="row">
|
|
387
|
-
<span><b style="color
|
|
440
|
+
<span><b style="color:var(--text)">${esc(u.name)}</b> <span style="color:var(--muted);font-size:0.72rem">${u.dir ? '📁 ' + esc(u.dir) : '不限目录'}</span></span>
|
|
388
441
|
<div class="row-actions">
|
|
389
|
-
<button onclick="editUserPwd('${esc(u.name)}')"
|
|
442
|
+
<button onclick="editUserPwd('${esc(u.name)}')">重置密码</button>
|
|
390
443
|
<button class="del" onclick="delUser('${esc(u.name)}')">删除</button>
|
|
391
444
|
</div>
|
|
392
445
|
</div>
|
|
393
446
|
`).join('');
|
|
394
447
|
}
|
|
395
448
|
|
|
449
|
+
function openAddMember() {
|
|
450
|
+
document.getElementById('add-member').style.display = '';
|
|
451
|
+
document.getElementById('add-open').style.display = 'none';
|
|
452
|
+
document.getElementById('u-name').focus();
|
|
453
|
+
}
|
|
454
|
+
function closeAddMember() {
|
|
455
|
+
document.getElementById('add-member').style.display = 'none';
|
|
456
|
+
document.getElementById('add-open').style.display = '';
|
|
457
|
+
document.getElementById('dir-pick').style.display = 'none';
|
|
458
|
+
}
|
|
459
|
+
function genPwd() {
|
|
460
|
+
const cs = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
461
|
+
const a = crypto.getRandomValues(new Uint32Array(8));
|
|
462
|
+
document.getElementById('u-pwd').value = [...a].map(n => cs[n % cs.length]).join('');
|
|
463
|
+
}
|
|
464
|
+
// 目录点选:复用 /api/dirs(主站以管理员身份登录时可用;不行就手动输入)
|
|
465
|
+
async function pickDir(path) {
|
|
466
|
+
const box = document.getElementById('dir-pick');
|
|
467
|
+
box.style.display = '';
|
|
468
|
+
try {
|
|
469
|
+
const r = await fetch('/api/dirs' + (path ? '?path=' + encodeURIComponent(path) : ''));
|
|
470
|
+
const d = await r.json();
|
|
471
|
+
if (!r.ok) throw new Error(d.error || r.status);
|
|
472
|
+
document.getElementById('u-dir').value = d.current;
|
|
473
|
+
box.innerHTML = `<div class="cur">${esc(d.current)}</div>` +
|
|
474
|
+
(d.parent ? `<div data-p="${esc(d.parent)}">⬆ 上一级</div>` : '') +
|
|
475
|
+
(d.drives || []).map(x => `<div data-p="${esc(x)}">💽 ${esc(x)}</div>`).join('') +
|
|
476
|
+
d.dirs.map(x => `<div data-p="${esc(x.path)}">📁 ${esc(x.name)}</div>`).join('');
|
|
477
|
+
box.querySelectorAll('[data-p]').forEach(el => el.onclick = () => pickDir(el.dataset.p));
|
|
478
|
+
} catch (e) { box.innerHTML = `<div class="cur">无法浏览目录(${esc(e.message)}),请直接输入路径</div>`; }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function setExclusive(on) {
|
|
482
|
+
try {
|
|
483
|
+
const r = await fetch('/api/admin/exclusive', { method: 'POST', headers: H(), body: JSON.stringify({ on }) });
|
|
484
|
+
const d = await r.json();
|
|
485
|
+
if (!d.ok) { alert(d.error || '失败'); refresh(); }
|
|
486
|
+
} catch { alert('连接失败'); refresh(); }
|
|
487
|
+
}
|
|
488
|
+
|
|
396
489
|
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
397
490
|
|
|
398
491
|
async function addUser() {
|
|
@@ -400,17 +493,27 @@ async function addUser() {
|
|
|
400
493
|
const name = document.getElementById('u-name').value.trim();
|
|
401
494
|
const dir = document.getElementById('u-dir').value.trim();
|
|
402
495
|
const msg = document.getElementById('u-msg');
|
|
403
|
-
if (!
|
|
496
|
+
if (!name || !pwd) { msg.className = 'msg msg-err'; msg.textContent = '成员名称和登录密码必填'; return; }
|
|
497
|
+
// 第一个成员 = 从单用户切到多用户:先讲清楚会发生什么
|
|
498
|
+
if (!_users.length && !confirm(
|
|
499
|
+
'添加第一个成员后,myhi 进入「多用户模式」:\n\n' +
|
|
500
|
+
'· 成员用各自的密码登录' + (dir ? ',只能在绑定的目录里建任务' : '') + ';\n' +
|
|
501
|
+
'· 成员可以升级、重启,但不能改全局设置(本机显示名、语音、用户、网关等);\n' +
|
|
502
|
+
'· 你(管理员)照常用管理密码或二维码登录,权限不变。\n\n' +
|
|
503
|
+
'以后删除全部成员即回到单用户模式。确定添加?')) return;
|
|
404
504
|
try {
|
|
405
505
|
const r = await fetch('/api/admin/user', { method: 'POST', headers: H(), body: JSON.stringify({ password: pwd, name, dir }) });
|
|
406
506
|
const d = await r.json();
|
|
407
|
-
if (d.ok) {
|
|
408
|
-
|
|
507
|
+
if (d.ok) {
|
|
508
|
+
msg.className = 'msg msg-ok'; msg.textContent = `已添加「${name}」,登录密码:${pwd}(请告知对方)`;
|
|
509
|
+
['u-pwd', 'u-name', 'u-dir'].forEach(id => document.getElementById(id).value = '');
|
|
510
|
+
closeAddMember(); refresh();
|
|
511
|
+
} else { msg.className = 'msg msg-err'; msg.textContent = d.error || '失败'; }
|
|
409
512
|
} catch { msg.className = 'msg msg-err'; msg.textContent = '连接失败'; }
|
|
410
513
|
}
|
|
411
514
|
|
|
412
515
|
async function editUserPwd(name) {
|
|
413
|
-
const newPwd = prompt(
|
|
516
|
+
const newPwd = prompt(`为 ${name} 设置新的登录密码(至少 4 位,设置后他当前的登录会失效):`);
|
|
414
517
|
if (!newPwd) return;
|
|
415
518
|
try {
|
|
416
519
|
const r = await fetch('/api/admin/user/password', { method: 'POST', headers: H(), body: JSON.stringify({ name, password: newPwd }) });
|
|
@@ -421,7 +524,7 @@ async function editUserPwd(name) {
|
|
|
421
524
|
}
|
|
422
525
|
|
|
423
526
|
async function delUser(name) {
|
|
424
|
-
if (!confirm(
|
|
527
|
+
if (!confirm(`确定删除成员「${name}」?他当前的登录会立即失效。` + (_users.length === 1 ? '\n\n这是最后一个成员,删除后回到单用户模式。' : ''))) return;
|
|
425
528
|
try {
|
|
426
529
|
const r = await fetch('/api/admin/user', { method: 'DELETE', headers: H(), body: JSON.stringify({ name }) });
|
|
427
530
|
const d = await r.json();
|
|
@@ -476,10 +579,14 @@ async function doUpgrade(force) {
|
|
|
476
579
|
} catch { fail('连接失败'); }
|
|
477
580
|
}
|
|
478
581
|
|
|
582
|
+
let _clInstalled = true;
|
|
479
583
|
async function doClaudeUpgrade() {
|
|
480
584
|
const btn = document.getElementById('btn-cl-up'), msg = document.getElementById('cl-msg');
|
|
481
|
-
|
|
482
|
-
|
|
585
|
+
const verb = _clInstalled ? '升级' : '安装';
|
|
586
|
+
if (!confirm(_clInstalled
|
|
587
|
+
? '升级 Claude Code?\nmyhi 会用 npm --include=optional 装全平台二进制,避免占位符坑。\n不影响正在跑的会话,升级后新查询生效。'
|
|
588
|
+
: '安装 Claude Code(myhi 的 AI 引擎)?\n下载约 260MB,需要几分钟。装好后还要在这台电脑上运行一次 claude 登录,或配置下方的大模型网关。')) return;
|
|
589
|
+
btn.disabled = true; btn.textContent = verb + '中...'; msg.className = 'status-msg'; msg.textContent = '正在下载安装(~260MB,可能几分钟)...'; msg.style.display = '';
|
|
483
590
|
try {
|
|
484
591
|
const r = await fetch('/api/claude/update', { method: 'POST', headers: H() });
|
|
485
592
|
const d = await r.json();
|
|
@@ -490,7 +597,7 @@ async function doClaudeUpgrade() {
|
|
|
490
597
|
const s = await sr.json();
|
|
491
598
|
if (!s.running) {
|
|
492
599
|
clearInterval(poll);
|
|
493
|
-
if (s.ok) { msg.textContent = '已升级到 v' + s.version; setTimeout(refresh, 500); }
|
|
600
|
+
if (s.ok) { msg.textContent = (_clInstalled ? '已升级到 v' : '已安装 v') + s.version + (_clInstalled ? '' : '。还差一步:在这台电脑上运行一次 claude 登录,或配置下方的大模型网关'); setTimeout(refresh, 500); }
|
|
494
601
|
else { msg.textContent = '失败: ' + (s.error||''); btn.disabled = false; btn.textContent = '重试'; }
|
|
495
602
|
}
|
|
496
603
|
} catch {}
|
|
@@ -515,7 +622,15 @@ async function waitReload() {
|
|
|
515
622
|
document.getElementById('op-msg').textContent = '重启超时,请手动刷新';
|
|
516
623
|
}
|
|
517
624
|
|
|
518
|
-
|
|
625
|
+
// 主站已是管理员 → 直接换后台令牌免登录;否则沿用本标签页已有令牌(重启后会失效)或显示登录框
|
|
626
|
+
(async () => {
|
|
627
|
+
try {
|
|
628
|
+
const r = await fetch('/api/admin/sso', { method: 'POST' });
|
|
629
|
+
const d = r.ok ? await r.json() : null;
|
|
630
|
+
if (d?.ok) { T = d.token; sessionStorage.setItem('myhi_admin', T); return showAdmin(); }
|
|
631
|
+
} catch {}
|
|
632
|
+
if (T) showAdmin();
|
|
633
|
+
})();
|
|
519
634
|
</script>
|
|
520
635
|
</body>
|
|
521
636
|
</html>
|