@wendongfly/myhi 1.3.200 → 1.3.202
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 +2 -3
- package/bin/install-check.js +27 -1
- package/bin/myhi.js +79 -23
- package/dist/admin.html +12 -10
- package/dist/index.html +33 -4
- package/dist/index.js +9 -9
- package/dist/index.min.js +156 -156
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
import { dirname, join } from 'path';
|
|
10
10
|
import { writeFileSync, readFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
|
-
import { checkInstall, upgradeStatusFile } from './install-check.js';
|
|
12
|
+
import { checkInstall, upgradeStatusFile, npmUpgradeCmd } from './install-check.js';
|
|
13
13
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = dirname(__filename);
|
|
@@ -129,8 +129,7 @@ function doUpgrade(target) {
|
|
|
129
129
|
const from = (() => { try { return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version; } catch { return null; } })();
|
|
130
130
|
log(`收到升级请求(${from} → ${target || 'latest'}),先停 server 释放文件占用...`);
|
|
131
131
|
stopChild(() => {
|
|
132
|
-
const cmd = (
|
|
133
|
-
' install -g @wendongfly/myhi@latest --registry https://registry.npmjs.org';
|
|
132
|
+
const cmd = npmUpgradeCmd(pkgRoot); // 同一套 Node 的 npm + 装回 myhi 当前目录(见 install-check.js)
|
|
134
133
|
log('执行: ' + cmd);
|
|
135
134
|
exec(cmd, { timeout: 600000, encoding: 'utf8', windowsHide: true }, (err, stdout, stderr) => {
|
|
136
135
|
let result;
|
package/bin/install-check.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 都会留下缺文件的半成品——带着它重启,新进程起不来,服务就挂了(测试机缺 dist/login.html 即此类)。
|
|
3
3
|
// server(网页升级)和 daemon(Windows 停服升级)共用。
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
5
|
+
import { join, dirname, basename, resolve } from 'path';
|
|
6
6
|
|
|
7
7
|
export const REQUIRED_FILES = [
|
|
8
8
|
'package.json', 'bin/myhi.js', 'bin/daemon.js',
|
|
@@ -23,3 +23,29 @@ export function checkInstall(pkgRoot, expectVersion) {
|
|
|
23
23
|
|
|
24
24
|
// 上次升级结果,落 ~/.myhi/upgrade-status.json:重启后的新进程(或 daemon 停服升级时)靠它把成败告诉网页
|
|
25
25
|
export function upgradeStatusFile(configDir) { return join(configDir, 'upgrade-status.json'); }
|
|
26
|
+
|
|
27
|
+
// ── 升级命令(server 网页升级、daemon 停服升级共用)────────────────────────
|
|
28
|
+
// 以前直接跑 PATH 里的 `npm install -g`:机器上装了不止一套 Node(典型:hibox 自带 Node 在 D:\hibox\nodejs,
|
|
29
|
+
// 系统另装了一套)时,PATH 里先找到的 npm 会把新版装进**它自己的**全局目录,而 myhi 从原目录重启
|
|
30
|
+
// → 永远还是旧版,网页报「npm 可能装到了别的目录」。现在:
|
|
31
|
+
// ① 优先用和当前 myhi **同一套 Node** 自带的 npm(node 可执行文件旁边的 npm / npm.cmd);
|
|
32
|
+
// ② 用 --prefix 明确装回 myhi **当前所在**的全局目录,原地覆盖。
|
|
33
|
+
// 认不出全局安装布局(如开发时从源码目录跑)就不加 --prefix,行为同以前。
|
|
34
|
+
|
|
35
|
+
/** pkgRoot 所在的 npm 全局前缀;Windows:<prefix>\node_modules\@wendongfly\myhi,其他:<prefix>/lib/node_modules/@wendongfly/myhi */
|
|
36
|
+
export function globalPrefixOf(pkgRoot) {
|
|
37
|
+
const r = resolve(pkgRoot);
|
|
38
|
+
const nm = dirname(dirname(r));
|
|
39
|
+
if (basename(dirname(r)) !== '@wendongfly' || basename(nm) !== 'node_modules') return null;
|
|
40
|
+
if (process.platform === 'win32') return dirname(nm);
|
|
41
|
+
return basename(dirname(nm)) === 'lib' ? dirname(dirname(nm)) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function npmUpgradeCmd(pkgRoot) {
|
|
45
|
+
const win = process.platform === 'win32';
|
|
46
|
+
const local = join(dirname(process.execPath), win ? 'npm.cmd' : 'npm');
|
|
47
|
+
const npm = existsSync(local) ? `"${local}"` : (win ? 'npm.cmd' : 'npm');
|
|
48
|
+
const prefix = globalPrefixOf(pkgRoot);
|
|
49
|
+
// 显式官方 registry:更新检查读官方源,安装若走镜像(同步延迟)会「一直提示新版却装到旧版」
|
|
50
|
+
return `${npm} install -g @wendongfly/myhi@latest --registry https://registry.npmjs.org${prefix ? ` --prefix "${prefix}"` : ''}`;
|
|
51
|
+
}
|
package/bin/myhi.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'child_process';
|
|
2
|
+
import { spawn, execSync } from 'child_process';
|
|
3
|
+
import { connect } from 'net';
|
|
3
4
|
import { fileURLToPath } from 'url';
|
|
4
5
|
import { dirname, join } from 'path';
|
|
5
6
|
import { mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
@@ -28,6 +29,9 @@ for (let i = 0; i < args.length; i++) {
|
|
|
28
29
|
httpOnlyFlag = true; args.splice(i, 1); i--;
|
|
29
30
|
} else if (args[i] === '--https') {
|
|
30
31
|
httpOnlyFlag = false; args.splice(i, 1); i--;
|
|
32
|
+
} else if (!cmd && (args[i] === 'http' || args[i] === 'https')) {
|
|
33
|
+
// 不带横杠也认:`myhi http` / `myhi https`(以前会被当成「前台启动」,协议根本没切)
|
|
34
|
+
httpOnlyFlag = args[i] === 'http'; args.splice(i, 1); i--;
|
|
31
35
|
} else if (!cmd && !args[i].startsWith('-')) {
|
|
32
36
|
cmd = args[i];
|
|
33
37
|
}
|
|
@@ -60,7 +64,8 @@ if (httpOnlyFlag !== null) {
|
|
|
60
64
|
try { cfg = JSON.parse(readFileSync(configFile, 'utf8').replace(/^\uFEFF/, '')); } catch {}
|
|
61
65
|
cfg.httpOnly = httpOnlyFlag;
|
|
62
66
|
writeFileSync(configFile, JSON.stringify(cfg, null, 2));
|
|
63
|
-
console.log(`[myhi] 已切换为 ${httpOnlyFlag ? 'HTTP(无 TLS,麦克风/摄像头等在非 localhost 会受限)' : 'HTTPS'}
|
|
67
|
+
console.log(`[myhi] 已切换为 ${httpOnlyFlag ? 'HTTP(无 TLS,麦克风/摄像头等在非 localhost 会受限)' : 'HTTPS'} 模式,访问地址:${httpOnlyFlag ? 'http' : 'https'}://localhost:${process.env.PORT || 12300}/`);
|
|
68
|
+
console.log(`[myhi] 配置写在 ${configFile}。若 myhi 是被别的程序(如 hibox)拉起的、这里改了不生效:在网页 设置 › 服务 › 访问协议 里切`);
|
|
64
69
|
} catch (e) {
|
|
65
70
|
console.error('[myhi] 写入 config.json 失败:', e.message);
|
|
66
71
|
process.exit(1);
|
|
@@ -93,22 +98,56 @@ function readPid() {
|
|
|
93
98
|
catch { return null; }
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
// 进程状态:alive / denied / dead。
|
|
102
|
+
// denied:进程在,但当前权限探测不了 —— Windows 上 myhi 若是在「以管理员身份运行」的命令行里启动的,
|
|
103
|
+
// 普通命令行 process.kill(pid, 0) 会得到 EPERM;以前把它当成「不存在」,status 就误报「未运行」
|
|
104
|
+
function procState(pid) {
|
|
105
|
+
if (!pid) return 'dead';
|
|
106
|
+
try { process.kill(pid, 0); return 'alive'; }
|
|
107
|
+
catch (e) { return e.code === 'EPERM' ? 'denied' : 'dead'; }
|
|
108
|
+
}
|
|
109
|
+
function isRunning(pid) { return procState(pid) !== 'dead'; }
|
|
110
|
+
const ELEVATED_TIP = process.platform === 'win32'
|
|
111
|
+
? '请在「以管理员身份运行」的命令行里执行'
|
|
112
|
+
: '请用启动它的同一用户(如 sudo)执行';
|
|
113
|
+
|
|
114
|
+
// 端口上有没有程序在监听(pid 文件不在 / 不是本账号启动的 myhi 时,用来给出提示)
|
|
115
|
+
function portOpen(port) {
|
|
116
|
+
return new Promise((r) => {
|
|
117
|
+
const sk = connect({ port, host: '127.0.0.1' });
|
|
118
|
+
const done = (v) => { sk.destroy(); r(v); };
|
|
119
|
+
sk.once('connect', () => done(true)); sk.once('error', () => done(false));
|
|
120
|
+
sk.setTimeout(1500, () => done(false));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function listenerPid(port) {
|
|
124
|
+
try {
|
|
125
|
+
if (process.platform === 'win32') {
|
|
126
|
+
const out = execSync('netstat -ano -p tcp', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, windowsHide: true });
|
|
127
|
+
const line = out.split('\n').find((l) => /LISTENING/i.test(l) && new RegExp(':' + port + '\\s').test(l));
|
|
128
|
+
return line ? parseInt(line.trim().split(/\s+/).pop(), 10) || null : null;
|
|
129
|
+
}
|
|
130
|
+
const out = execSync(`ss -ltnp 'sport = :${port}'`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
131
|
+
const m = /pid=(\d+)/.exec(out); return m ? parseInt(m[1], 10) : null;
|
|
132
|
+
} catch { return null; }
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
function stopDaemon() {
|
|
102
136
|
const serverPidFile = join(configDir, 'server.pid');
|
|
103
|
-
let stopped = false;
|
|
137
|
+
let stopped = false, denied = false;
|
|
104
138
|
|
|
105
139
|
// 停止 daemon 进程
|
|
106
140
|
const daemonPid = readPid();
|
|
107
141
|
if (daemonPid && isRunning(daemonPid)) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
try {
|
|
143
|
+
process.kill(daemonPid, 'SIGTERM');
|
|
144
|
+
try { unlinkSync(pidFile); } catch {}
|
|
145
|
+
console.log(`[myhi] 已停止 daemon (PID ${daemonPid})`);
|
|
146
|
+
stopped = true;
|
|
147
|
+
} catch {
|
|
148
|
+
console.log(`[myhi] 停不掉 daemon (PID ${daemonPid}):权限不够(它可能是以管理员身份或别的账号启动的),${ELEVATED_TIP} myhi stop`);
|
|
149
|
+
denied = true;
|
|
150
|
+
}
|
|
112
151
|
} else if (daemonPid) {
|
|
113
152
|
try { unlinkSync(pidFile); } catch {}
|
|
114
153
|
}
|
|
@@ -117,20 +156,25 @@ function stopDaemon() {
|
|
|
117
156
|
try {
|
|
118
157
|
const serverPid = parseInt(readFileSync(serverPidFile, 'utf8').trim(), 10);
|
|
119
158
|
if (serverPid && isRunning(serverPid)) {
|
|
120
|
-
|
|
121
|
-
|
|
159
|
+
let ok = true;
|
|
160
|
+
try {
|
|
161
|
+
if (process.platform === 'win32') execSync(`taskkill /PID ${serverPid} /F /T`, { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
162
|
+
else process.kill(serverPid, 'SIGTERM');
|
|
163
|
+
} catch { ok = false; }
|
|
164
|
+
if (ok) {
|
|
165
|
+
try { unlinkSync(serverPidFile); } catch {}
|
|
166
|
+
console.log(`[myhi] 已停止 server (PID ${serverPid})`);
|
|
167
|
+
stopped = true;
|
|
122
168
|
} else {
|
|
123
|
-
|
|
169
|
+
console.log(`[myhi] 停不掉 server (PID ${serverPid}):权限不够(它可能是以管理员身份或别的账号启动的),${ELEVATED_TIP} myhi stop`);
|
|
170
|
+
denied = true;
|
|
124
171
|
}
|
|
125
|
-
try { unlinkSync(serverPidFile); } catch {}
|
|
126
|
-
console.log(`[myhi] 已停止 server (PID ${serverPid})`);
|
|
127
|
-
stopped = true;
|
|
128
172
|
} else if (serverPid) {
|
|
129
173
|
try { unlinkSync(serverPidFile); } catch {}
|
|
130
174
|
}
|
|
131
175
|
} catch {}
|
|
132
176
|
|
|
133
|
-
if (!stopped) console.log('[myhi] 没有正在运行的 myhi 进程');
|
|
177
|
+
if (!stopped && !denied) console.log('[myhi] 没有正在运行的 myhi 进程');
|
|
134
178
|
return stopped;
|
|
135
179
|
}
|
|
136
180
|
|
|
@@ -196,14 +240,25 @@ if (cmd === 'attach') {
|
|
|
196
240
|
const daemonPid = readPid();
|
|
197
241
|
let serverPid = null;
|
|
198
242
|
try { serverPid = parseInt(readFileSync(join(configDir, 'server.pid'), 'utf8').trim(), 10); } catch {}
|
|
199
|
-
const
|
|
200
|
-
const
|
|
243
|
+
const dState = procState(daemonPid), sState = procState(serverPid);
|
|
244
|
+
const daemonAlive = dState !== 'dead';
|
|
245
|
+
const serverAlive = sState !== 'dead';
|
|
201
246
|
if (daemonAlive || serverAlive) {
|
|
202
247
|
const mode = daemonAlive ? 'daemon' : '前台';
|
|
203
248
|
const pid = daemonAlive ? daemonPid : serverPid;
|
|
204
|
-
|
|
249
|
+
let httpOnly = false; try { httpOnly = !!JSON.parse(readFileSync(configFile, 'utf8').replace(/^\uFEFF/, '')).httpOnly; } catch {}
|
|
250
|
+
console.log(`[myhi] v${pkgVersion} 运行中 (${mode}, PID ${pid}),访问地址 ${httpOnly ? 'http' : 'https'}://localhost:${process.env.PORT || 12300}/`);
|
|
251
|
+
if ((daemonAlive ? dState : sState) === 'denied') console.log(`[myhi] 注意:它是以更高权限(管理员 / 别的账号)启动的,${ELEVATED_TIP} myhi stop / restart 才能管理它`);
|
|
205
252
|
} else {
|
|
206
|
-
|
|
253
|
+
const port = parseInt(process.env.PORT || '12300', 10);
|
|
254
|
+
if (await portOpen(port)) {
|
|
255
|
+
const lp = listenerPid(port);
|
|
256
|
+
console.log(`[myhi] v${pkgVersion} 本账号没有记录到运行中的 myhi,但端口 ${port} 正在被${lp ? ` PID ${lp} ` : '某个程序'}监听`);
|
|
257
|
+
console.log(`[myhi] 多半是 myhi 以管理员身份、或用另一个系统账号启动的,${ELEVATED_TIP} myhi status / myhi stop`);
|
|
258
|
+
if (process.platform === 'win32' && lp) console.log(`[myhi] 也可以在管理员命令行里直接结束它:taskkill /PID ${lp} /F /T`);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(`[myhi] v${pkgVersion} 未运行`);
|
|
261
|
+
}
|
|
207
262
|
}
|
|
208
263
|
|
|
209
264
|
} else if (cmd === 'log' || cmd === 'logs') {
|
|
@@ -631,8 +686,9 @@ myhi — 基于 Web 的终端共享工具 (${platform})
|
|
|
631
686
|
myhi start 前台启动服务器
|
|
632
687
|
myhi -p <port> 指定监听端口(默认 12300)
|
|
633
688
|
myhi -d 后台启动(日志写入 ${configPath}daemon.log)
|
|
634
|
-
myhi
|
|
635
|
-
myhi
|
|
689
|
+
myhi http 切为纯 HTTP(无 TLS)并重启,持久生效(同 --http)
|
|
690
|
+
myhi https 切回 HTTPS(默认)并重启(同 --https)
|
|
691
|
+
也可在网页 设置 › 服务 › 访问协议 里点选
|
|
636
692
|
myhi stop 停止后台服务器
|
|
637
693
|
myhi restart 重启后台服务器
|
|
638
694
|
myhi status 查看运行状态
|
package/dist/admin.html
CHANGED
|
@@ -473,18 +473,20 @@ async function refresh() {
|
|
|
473
473
|
} catch {}
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
//
|
|
476
|
+
// 使用模式:0~1 个登录账号 = 单用户(那个账号就是主人,全部权限);≥2 个 = 多用户(账号变成成员,权限受限)
|
|
477
477
|
let _users = [];
|
|
478
478
|
function renderUsers(users, exclusive) {
|
|
479
479
|
_users = users;
|
|
480
|
-
const multi = users.length
|
|
480
|
+
const multi = users.length >= 2;
|
|
481
481
|
document.getElementById('mode-status').innerHTML = multi
|
|
482
482
|
? `<b>当前:多用户模式 · ${users.length} 个成员</b><small>成员用各自密码登录,可以升级 / 重启,不能改全局设置;你用管理密码或二维码登录,权限不变。</small>`
|
|
483
|
+
: users.length === 1
|
|
484
|
+
? `<b>当前:单用户模式 · 登录账号「${esc(users[0].name)}」</b><small>只有一个账号时它就是主人,用自己的密码登录、所有功能都能用,不需要来这里管理。再添加一个成员才进入多用户模式。</small>`
|
|
483
485
|
: `<b>当前:单用户模式</b><small>只有你在用,所有功能都能用。需要给别人用时再添加成员。</small>`;
|
|
484
|
-
document.getElementById('add-open').textContent = multi ? '+ 添加成员' : '+ 添加成员,开启多用户模式';
|
|
486
|
+
document.getElementById('add-open').textContent = multi ? '+ 添加成员' : users.length === 1 ? '+ 添加成员,开启多用户模式' : '+ 添加登录账号';
|
|
485
487
|
document.getElementById('excl-row').style.display = multi ? '' : 'none';
|
|
486
488
|
document.getElementById('excl-toggle').checked = !!exclusive;
|
|
487
|
-
document.getElementById('mode-foot').textContent = multi ? '
|
|
489
|
+
document.getElementById('mode-foot').textContent = multi ? '删到只剩一个账号即回到单用户模式(剩下的那个拥有全部权限)。删除成员或重置密码后,他当前的登录会立即失效。' : '';
|
|
488
490
|
const el = document.getElementById('user-list');
|
|
489
491
|
el.innerHTML = users.map(u => `
|
|
490
492
|
<div class="row">
|
|
@@ -545,13 +547,13 @@ async function addUser() {
|
|
|
545
547
|
const dir = document.getElementById('u-dir').value.trim();
|
|
546
548
|
const msg = document.getElementById('u-msg');
|
|
547
549
|
if (!name || !pwd) { msg.className = 'msg msg-err'; msg.textContent = '成员名称和登录密码必填'; return; }
|
|
548
|
-
//
|
|
549
|
-
if (
|
|
550
|
-
'
|
|
551
|
-
|
|
550
|
+
// 第二个账号 = 从单用户切到多用户:先讲清楚会发生什么(尤其原来那个账号会降为成员)
|
|
551
|
+
if (_users.length === 1 && !confirm(
|
|
552
|
+
'添加后共 2 个账号,myhi 进入「多用户模式」:\n\n' +
|
|
553
|
+
`· 原来的账号「${_users[0].name}」和新成员都变成「成员」,各用自己的密码登录` + (dir ? ',新成员只能在绑定的目录里建任务' : '') + ';\n' +
|
|
552
554
|
'· 成员可以升级、重启,但不能改全局设置(本机显示名、语音、用户、网关等);\n' +
|
|
553
555
|
'· 你(管理员)照常用管理密码或二维码登录,权限不变。\n\n' +
|
|
554
|
-
'
|
|
556
|
+
'以后删到只剩一个账号即回到单用户模式。确定添加?')) return;
|
|
555
557
|
try {
|
|
556
558
|
const r = await fetch('/api/admin/user', { method: 'POST', headers: H(), body: JSON.stringify({ password: pwd, name, dir }) });
|
|
557
559
|
const d = await r.json();
|
|
@@ -575,7 +577,7 @@ async function editUserPwd(name) {
|
|
|
575
577
|
}
|
|
576
578
|
|
|
577
579
|
async function delUser(name) {
|
|
578
|
-
if (!confirm(
|
|
580
|
+
if (!confirm(`确定删除「${name}」?他当前的登录会立即失效。` + (_users.length === 2 ? `\n\n删除后只剩「${_users.find(u => u.name !== name)?.name}」一个账号,回到单用户模式,它将拥有全部权限。` : _users.length === 1 ? '\n\n这是最后一个登录账号,删除后只能用管理密码登录。' : ''))) return;
|
|
579
581
|
try {
|
|
580
582
|
const r = await fetch('/api/admin/user', { method: 'DELETE', headers: H(), body: JSON.stringify({ name }) });
|
|
581
583
|
const d = await r.json();
|
package/dist/index.html
CHANGED
|
@@ -857,7 +857,7 @@
|
|
|
857
857
|
</div>
|
|
858
858
|
<ol class="hs-steps">
|
|
859
859
|
<li><span class="hs-num">1</span><div><b>建一个任务</b><p>点右下角 <kbd>+</kbd>,选「默认」预设、起个名字,点创建。</p></div></li>
|
|
860
|
-
<li><span class="hs-num">2</span><div><b>像发微信一样说需求</b><p
|
|
860
|
+
<li><span class="hs-num">2</span><div><b>像发微信一样说需求</b><p>进入任务就是对话界面,说出你的需求,AI 会理解您说的话,自己读文件、写文件、执行命令。</p></div></li>
|
|
861
861
|
<li><span class="hs-num">3</span><div><b>用 <kbd>@</kbd> 引用文件</b><p>消息里打 @ 弹出文件搜索,选中即插入路径,不用手打文件名。</p></div></li>
|
|
862
862
|
</ol>
|
|
863
863
|
<div class="hs-label">常用功能</div>
|
|
@@ -963,6 +963,11 @@
|
|
|
963
963
|
<button id="set-up-btn" class="set-btn dirty" style="display:none" onclick="homeUpgrade()">升级</button>
|
|
964
964
|
</div>
|
|
965
965
|
<div id="set-up-status" class="set-status"></div>
|
|
966
|
+
<div class="set-item admin-only" id="set-proto-row">
|
|
967
|
+
<div class="set-lbl">访问协议<small id="set-proto-sub">HTTPS 才能在手机上用麦克风 / 摄像头;内网只想少点「不安全」提示可用 HTTP</small></div>
|
|
968
|
+
<div class="set-seg" id="set-proto"><button data-p="https">HTTPS</button><button data-p="http">HTTP</button></div>
|
|
969
|
+
</div>
|
|
970
|
+
<div id="set-proto-status" class="set-status"></div>
|
|
966
971
|
<a id="set-admin-link" class="set-link admin-only" href="/admin">🛠 管理后台 <small>用户 · 网关 · 密钥 · Claude 升级</small></a>
|
|
967
972
|
<button class="set-link" onclick="restartMyhi()">🔄 重启 myhi</button>
|
|
968
973
|
</div>
|
|
@@ -1107,13 +1112,14 @@
|
|
|
1107
1112
|
// 成员用户(users.json 登录)才显示自助改密入口;管理员改密码在 /admin
|
|
1108
1113
|
// 改全局配置只给管理员(后端同样拦):成员的「服务」组没有管理后台入口,本机显示名与语音只读
|
|
1109
1114
|
_isAdmin = (data.role || 'admin') === 'admin';
|
|
1110
|
-
|
|
1115
|
+
// 0~1 个登录账号是单用户(hibox 默认建的那个账号就是主人),≥2 个才是多用户
|
|
1116
|
+
document.getElementById('set-mode').innerHTML = !data.multiUser ? '👤 单用户模式 · 所有功能都可用'
|
|
1111
1117
|
: _isAdmin ? '👥 多用户模式 · 你是管理员 · <a href="/admin#mode">管理成员 →</a>'
|
|
1112
1118
|
: `👥 多用户模式 · 你是成员「${esc(data.name || '')}」· 本机显示名、语音等全局设置需管理员修改`;
|
|
1113
1119
|
if (!_isAdmin) document.querySelectorAll('#set-svc .admin-only').forEach(el => el.remove());
|
|
1114
1120
|
checkAiEngine();
|
|
1115
1121
|
checkHomeVersion();
|
|
1116
|
-
if (data.hasUsers && data.
|
|
1122
|
+
if (data.hasUsers && data.member) { document.getElementById('passwd-btn').style.display = ''; document.getElementById('ws-menu-pw').style.display = ''; }
|
|
1117
1123
|
// 单用户模式没有「登录账号」可退:⚙ 菜单不显示账户段
|
|
1118
1124
|
if (!(data.exclusive || data.hasUsers)) document.getElementById('ws-menu-acct').style.display = 'none';
|
|
1119
1125
|
if (data.dir) {
|
|
@@ -1302,7 +1308,8 @@
|
|
|
1302
1308
|
|
|
1303
1309
|
window.openSettings = function() {
|
|
1304
1310
|
renderAppearance();
|
|
1305
|
-
['set-machine-status', 'set-asr-status', 'set-restart-status', 'set-up-status'].filter(id => document.getElementById(id)).forEach(id => setStatus(id, ''));
|
|
1311
|
+
['set-machine-status', 'set-asr-status', 'set-restart-status', 'set-up-status', 'set-proto-status'].filter(id => document.getElementById(id)).forEach(id => setStatus(id, ''));
|
|
1312
|
+
if (_isAdmin) loadProto();
|
|
1306
1313
|
document.querySelectorAll('#set-sheet .set-btn').forEach(b => b.classList.remove('dirty'));
|
|
1307
1314
|
document.getElementById('set-backdrop').classList.add('open');
|
|
1308
1315
|
document.getElementById('set-sheet').classList.add('open');
|
|
@@ -1456,6 +1463,28 @@
|
|
|
1456
1463
|
} catch (e) { btn.disabled = false; setStatus('set-up-status', '升级失败:' + e.message, 'red'); }
|
|
1457
1464
|
};
|
|
1458
1465
|
|
|
1466
|
+
// 访问协议切换:由正在运行的 myhi 改它自己的配置并重启,重启后跳到新地址
|
|
1467
|
+
async function loadProto() {
|
|
1468
|
+
let d; try { d = await (await fetch('/api/server-mode')).json(); } catch { return; }
|
|
1469
|
+
if (!d.ok) return;
|
|
1470
|
+
const seg = document.getElementById('set-proto');
|
|
1471
|
+
seg.querySelectorAll('button').forEach(b => {
|
|
1472
|
+
b.classList.toggle('on', (b.dataset.p === 'http') === d.httpOnly);
|
|
1473
|
+
b.onclick = () => switchProto(b.dataset.p === 'http', d);
|
|
1474
|
+
});
|
|
1475
|
+
if (d.fromEnv) setStatus('set-proto-status', '启动时设了环境变量 MYHI_NO_HTTPS,固定为 HTTP');
|
|
1476
|
+
}
|
|
1477
|
+
async function switchProto(httpOnly, cur) {
|
|
1478
|
+
if (httpOnly === cur.httpOnly) return;
|
|
1479
|
+
const url = `${httpOnly ? 'http' : 'https'}://${location.hostname}:${cur.port}/`;
|
|
1480
|
+
if (!confirm(`切换为 ${httpOnly ? 'HTTP' : 'HTTPS'}?\n\nmyhi 会重启(约 5 秒,正在执行的任务会被打断这一轮),之后访问地址变为:\n${url}\n\n${httpOnly ? '注意:HTTP 下手机等非本机浏览器不能用麦克风 / 摄像头(语音输入、拍照)。' : '注意:HTTPS 用的是自签名证书,浏览器会提示「不安全」,点「继续访问」即可。'}`)) return;
|
|
1481
|
+
let d; try { d = await (await fetch('/api/server-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ httpOnly }) })).json(); } catch (e) { d = { ok: false, error: e.message }; }
|
|
1482
|
+
if (!d.ok) { setStatus('set-proto-status', '切换失败:' + (d.error || '')); return; }
|
|
1483
|
+
let n = 8;
|
|
1484
|
+
const tick = () => { setStatus('set-proto-status', `myhi 正在重启,${n} 秒后自动打开 ${url}(没跳转就手动打开它)`); if (n-- <= 0) location.href = url; else setTimeout(tick, 1000); };
|
|
1485
|
+
tick();
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1459
1488
|
window.restartMyhi = async function() {
|
|
1460
1489
|
if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
|
|
1461
1490
|
try { await fetch('/api/restart', { method: 'POST' }); } catch {}
|