@wendongfly/zihi 1.1.13 → 1.1.15
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 +3 -19
- package/bin/zihi.js +243 -22
- package/dist/781.index.js +154 -0
- package/dist/admin.html +51 -0
- package/dist/attach.js +1 -1
- package/dist/client-dist/socket.io.esm.min.js +1 -1
- package/dist/client-dist/socket.io.esm.min.js.map +1 -0
- package/dist/client-dist/socket.io.js +1 -1
- package/dist/client-dist/socket.io.js.map +1 -0
- package/dist/client-dist/socket.io.min.js +1 -1
- package/dist/client-dist/socket.io.min.js.map +1 -0
- package/dist/client-dist/socket.io.msgpack.min.js +1 -1
- package/dist/client-dist/socket.io.msgpack.min.js.map +1 -0
- package/dist/index.js +214 -286
- package/dist/index.min.js +399 -0
- package/dist/package.json +1 -1
- package/package.json +2 -1
package/bin/daemon.js
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* zihi
|
|
3
|
+
* zihi 守护进程:标记为后台模式并启动 server。
|
|
4
|
+
* PID 文件的写入/清理由 server 自己负责(写的是完整 JSON 元信息)。
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
-
import { join, dirname } from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const pidFile = join(homedir(), '.zihi', 'daemon.pid');
|
|
12
|
-
|
|
13
|
-
// 写入自身 PID
|
|
14
|
-
writeFileSync(pidFile, String(process.pid));
|
|
15
|
-
|
|
16
|
-
// 退出时清理 PID 文件
|
|
17
|
-
function cleanup() { try { unlinkSync(pidFile); } catch {} }
|
|
18
|
-
process.on('exit', cleanup);
|
|
19
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
20
|
-
process.on('SIGINT', () => process.exit(0));
|
|
21
|
-
|
|
22
|
-
// 启动 server
|
|
6
|
+
process.env.ZIHI_DAEMON = '1';
|
|
23
7
|
await import('../dist/index.js');
|
package/bin/zihi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'child_process';
|
|
2
|
+
import { spawn, execSync } from 'child_process';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
@@ -30,31 +30,178 @@ const logFile = join(configDir, 'daemon.log');
|
|
|
30
30
|
|
|
31
31
|
// ── 工具函数 ──────────────────────────────────────────────
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/** 读取 pidfile,兼容 JSON(新格式)和纯 PID 数字(旧格式)。 */
|
|
34
|
+
function readPidInfo() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = readFileSync(pidFile, 'utf8').trim();
|
|
37
|
+
if (!raw) return null;
|
|
38
|
+
if (raw.startsWith('{')) return JSON.parse(raw);
|
|
39
|
+
const pid = parseInt(raw, 10);
|
|
40
|
+
return pid ? { pid } : null;
|
|
41
|
+
} catch { return null; }
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
function
|
|
44
|
+
function isPidAlive(pid) {
|
|
39
45
|
try { process.kill(pid, 0); return true; }
|
|
40
46
|
catch { return false; }
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
/** 查进程命令行是否含 zihi 标识,防 PID 复用导致误判成别的 node 进程。 */
|
|
50
|
+
function pidCmdlineLooksLikeZihi(pid) {
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const out = execSync(
|
|
54
|
+
`wmic process where "ProcessId=${pid}" get CommandLine /format:list`,
|
|
55
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }
|
|
56
|
+
);
|
|
57
|
+
return /zihi/i.test(out) && /(daemon\.js|dist[\\/]index\.js)/i.test(out);
|
|
58
|
+
} else {
|
|
59
|
+
const out = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
60
|
+
return /zihi/i.test(out) && /(daemon\.js|dist\/index\.js)/i.test(out);
|
|
61
|
+
}
|
|
62
|
+
} catch { return false; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 向运行中的实例发健康探针:确认签名为 zihi 且 pid 匹配。 */
|
|
66
|
+
async function probeZihiPing(port, timeoutMs = 1500) {
|
|
67
|
+
if (!port) return null;
|
|
68
|
+
try {
|
|
69
|
+
const ctrl = new AbortController();
|
|
70
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
71
|
+
const resp = await fetch(`http://127.0.0.1:${port}/api/zihi-ping`, { signal: ctrl.signal });
|
|
72
|
+
clearTimeout(t);
|
|
73
|
+
if (!resp.ok) return null;
|
|
74
|
+
const data = await resp.json();
|
|
75
|
+
return data && data.zihi === true ? data : null;
|
|
76
|
+
} catch { return null; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 校验当前 pidfile 指向的是否确实是一个活着的 zihi 实例。
|
|
81
|
+
* 两层校验:HTTP 探针 为主,cmdline 检查 为辅。
|
|
82
|
+
*/
|
|
83
|
+
async function verifyRunning() {
|
|
84
|
+
const info = readPidInfo();
|
|
85
|
+
if (!info || !info.pid) return null;
|
|
86
|
+
if (!isPidAlive(info.pid)) return null;
|
|
87
|
+
|
|
88
|
+
// 主校验:健康探针
|
|
89
|
+
const ping = await probeZihiPing(info.port);
|
|
90
|
+
if (ping && ping.pid === info.pid) {
|
|
91
|
+
return { ...info, ...ping, verifiedBy: 'http' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 辅校验:命令行关键字
|
|
95
|
+
if (pidCmdlineLooksLikeZihi(info.pid)) {
|
|
96
|
+
return { ...info, verifiedBy: 'cmdline' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatUptime(startedAt) {
|
|
103
|
+
if (!startedAt) return '未知';
|
|
104
|
+
const ms = Date.now() - startedAt;
|
|
105
|
+
const s = Math.floor(ms / 1000);
|
|
106
|
+
if (s < 60) return `${s}s`;
|
|
107
|
+
const m = Math.floor(s / 60);
|
|
108
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
109
|
+
const h = Math.floor(m / 60);
|
|
110
|
+
return `${h}h${m % 60}m`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printRunningInfo(info, opts = {}) {
|
|
114
|
+
const started = info.startedAt ? new Date(info.startedAt).toLocaleString() : '未知';
|
|
115
|
+
console.log(`[zihi] 服务已在运行,跳过启动`);
|
|
116
|
+
console.log(` PID: ${info.pid}`);
|
|
117
|
+
if (info.port) console.log(` 端口: ${info.port}`);
|
|
118
|
+
if (info.version) console.log(` 版本: v${info.version}`);
|
|
119
|
+
if (info.cwd) console.log(` 工作目录: ${info.cwd}`);
|
|
120
|
+
if (info.execPath) console.log(` Node: ${info.execPath}`);
|
|
121
|
+
console.log(` 启动时间: ${started} (运行 ${formatUptime(info.startedAt)})`);
|
|
122
|
+
console.log(` 校验方式: ${info.verifiedBy === 'http' ? 'HTTP 探针' : '命令行匹配'}`);
|
|
123
|
+
if (info.port) console.log(` 管理界面: http://localhost:${info.port}/admin`);
|
|
124
|
+
|
|
125
|
+
const n = opts.logLines ?? 20;
|
|
126
|
+
if (n > 0) {
|
|
127
|
+
try {
|
|
128
|
+
const content = readFileSync(logFile, 'utf8');
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
console.log(`\n ── 最近 ${n} 行日志 ──`);
|
|
131
|
+
console.log(lines.slice(-n).join('\n'));
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function stopDaemon() {
|
|
137
|
+
// Linux 下优先使用 systemd
|
|
138
|
+
if (hasSystemdService()) {
|
|
139
|
+
try {
|
|
140
|
+
execSync('sudo systemctl stop zihi', { stdio: 'inherit' });
|
|
141
|
+
console.log('[zihi] 服务已停止');
|
|
142
|
+
return true;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error('[zihi] systemctl 停止失败:', e.message);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const info = readPidInfo();
|
|
150
|
+
if (!info || !info.pid) { console.log('[zihi] 没有正在运行的后台进程'); return false; }
|
|
151
|
+
if (!isPidAlive(info.pid)) {
|
|
47
152
|
try { unlinkSync(pidFile); } catch {}
|
|
48
153
|
console.log('[zihi] 进程已不存在,已清理 PID 文件');
|
|
49
154
|
return false;
|
|
50
155
|
}
|
|
51
|
-
|
|
156
|
+
// 额外校验:确认 PID 确实是 zihi,避免 PID 复用后误杀别人
|
|
157
|
+
const verified = await verifyRunning();
|
|
158
|
+
if (!verified) {
|
|
159
|
+
try { unlinkSync(pidFile); } catch {}
|
|
160
|
+
console.log(`[zihi] PID ${info.pid} 存在但不是 zihi 进程,已清理陈旧 PID 文件`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
process.kill(info.pid, 'SIGTERM');
|
|
52
164
|
try { unlinkSync(pidFile); } catch {}
|
|
53
|
-
console.log(`[zihi] 已停止后台进程 (PID ${pid})`);
|
|
165
|
+
console.log(`[zihi] 已停止后台进程 (PID ${info.pid})`);
|
|
54
166
|
return true;
|
|
55
167
|
}
|
|
56
168
|
|
|
57
|
-
|
|
169
|
+
/** 检查是否已安装 systemd 服务 */
|
|
170
|
+
function hasSystemdService() {
|
|
171
|
+
if (process.platform !== 'linux') return false;
|
|
172
|
+
try {
|
|
173
|
+
execSync('systemctl cat zihi.service', { stdio: 'ignore', timeout: 2000 });
|
|
174
|
+
return true;
|
|
175
|
+
} catch { return false; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function startDaemon() {
|
|
179
|
+
// Linux 下优先使用 systemd
|
|
180
|
+
if (hasSystemdService()) {
|
|
181
|
+
console.log('[zihi] 检测到 systemd 服务,通过 systemctl 启动');
|
|
182
|
+
try {
|
|
183
|
+
execSync('sudo systemctl restart zihi', { stdio: 'inherit' });
|
|
184
|
+
console.log('[zihi] 服务已启动');
|
|
185
|
+
console.log(' 查看状态: sudo systemctl status zihi');
|
|
186
|
+
console.log(' 查看日志: sudo journalctl -u zihi -f');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('[zihi] systemctl 启动失败:', e.message);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 启动前唯一性校验
|
|
195
|
+
const running = await verifyRunning();
|
|
196
|
+
if (running) {
|
|
197
|
+
printRunningInfo(running);
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
// pidfile 存在但校验失败 → 清理陈旧文件
|
|
201
|
+
if (readPidInfo()) {
|
|
202
|
+
try { unlinkSync(pidFile); } catch {}
|
|
203
|
+
}
|
|
204
|
+
|
|
58
205
|
mkdirSync(configDir, { recursive: true });
|
|
59
206
|
const out = openSync(logFile, 'a');
|
|
60
207
|
// 启动守护进程(daemon.js),由它管理 server 子进程
|
|
@@ -103,21 +250,28 @@ if (cmd === 'attach') {
|
|
|
103
250
|
await import('../dist/attach.js');
|
|
104
251
|
|
|
105
252
|
} else if (cmd === 'stop') {
|
|
106
|
-
stopDaemon();
|
|
253
|
+
await stopDaemon();
|
|
107
254
|
|
|
108
255
|
} else if (cmd === 'restart') {
|
|
109
|
-
stopDaemon();
|
|
256
|
+
await stopDaemon();
|
|
110
257
|
await new Promise(r => setTimeout(r, 500));
|
|
111
|
-
startDaemon();
|
|
258
|
+
await startDaemon();
|
|
112
259
|
process.exit(0);
|
|
113
260
|
|
|
114
261
|
} else if (cmd === 'status') {
|
|
115
262
|
const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
console.log(`[zihi] v${pkgVersion}
|
|
263
|
+
const running = await verifyRunning();
|
|
264
|
+
if (running) {
|
|
265
|
+
console.log(`[zihi] v${pkgVersion} 运行中`);
|
|
266
|
+
printRunningInfo(running, { logLines: 0 });
|
|
119
267
|
} else {
|
|
120
|
-
|
|
268
|
+
const stale = readPidInfo();
|
|
269
|
+
if (stale) {
|
|
270
|
+
try { unlinkSync(pidFile); } catch {}
|
|
271
|
+
console.log(`[zihi] v${pkgVersion} 未运行(已清理陈旧 PID 文件)`);
|
|
272
|
+
} else {
|
|
273
|
+
console.log(`[zihi] v${pkgVersion} 未运行`);
|
|
274
|
+
}
|
|
121
275
|
}
|
|
122
276
|
|
|
123
277
|
} else if (cmd === 'log' || cmd === 'logs') {
|
|
@@ -130,6 +284,66 @@ if (cmd === 'attach') {
|
|
|
130
284
|
console.log('[zihi] 日志文件不存在');
|
|
131
285
|
}
|
|
132
286
|
|
|
287
|
+
} else if (cmd === 'install' || cmd === 'service') {
|
|
288
|
+
// Linux systemd 服务安装
|
|
289
|
+
if (process.platform !== 'linux') {
|
|
290
|
+
console.log('[zihi] systemd 服务安装仅支持 Linux');
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
const servicePath = '/etc/systemd/system/zihi.service';
|
|
294
|
+
const user = process.env.SUDO_USER || process.env.USER || 'root';
|
|
295
|
+
const nodeBin = process.execPath;
|
|
296
|
+
const zihiBin = join(__dirname, 'zihi.js');
|
|
297
|
+
const port = process.env.PORT || '12345';
|
|
298
|
+
const unit = `[Unit]
|
|
299
|
+
Description=ZiHi - AI Agent Terminal Sharing
|
|
300
|
+
After=network.target
|
|
301
|
+
|
|
302
|
+
[Service]
|
|
303
|
+
Type=simple
|
|
304
|
+
User=${user}
|
|
305
|
+
Environment=PORT=${port}
|
|
306
|
+
Environment=ZIHI_DAEMON=1
|
|
307
|
+
ExecStart=${nodeBin} ${join(__dirname, 'daemon.js')}
|
|
308
|
+
Restart=on-failure
|
|
309
|
+
RestartSec=5
|
|
310
|
+
StandardOutput=append:${join(homedir(), '.zihi/daemon.log')}
|
|
311
|
+
StandardError=append:${join(homedir(), '.zihi/daemon.log')}
|
|
312
|
+
|
|
313
|
+
[Install]
|
|
314
|
+
WantedBy=multi-user.target
|
|
315
|
+
`;
|
|
316
|
+
try {
|
|
317
|
+
execSync(`sudo tee ${servicePath}`, { input: unit, stdio: ['pipe', 'ignore', 'inherit'] });
|
|
318
|
+
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
|
|
319
|
+
execSync('sudo systemctl enable zihi', { stdio: 'inherit' });
|
|
320
|
+
execSync('sudo systemctl restart zihi', { stdio: 'inherit' });
|
|
321
|
+
console.log('\n[zihi] systemd 服务已安装并启动');
|
|
322
|
+
console.log(' 查看状态: sudo systemctl status zihi');
|
|
323
|
+
console.log(' 查看日志: sudo journalctl -u zihi -f');
|
|
324
|
+
console.log(' 停止服务: sudo systemctl stop zihi');
|
|
325
|
+
console.log(' 卸载服务: zihi uninstall');
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error('[zihi] 安装失败:', e.message);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
} else if (cmd === 'uninstall') {
|
|
332
|
+
if (process.platform !== 'linux') {
|
|
333
|
+
console.log('[zihi] systemd 服务卸载仅支持 Linux');
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
execSync('sudo systemctl stop zihi 2>/dev/null || true', { stdio: 'inherit' });
|
|
338
|
+
execSync('sudo systemctl disable zihi 2>/dev/null || true', { stdio: 'inherit' });
|
|
339
|
+
execSync('sudo rm -f /etc/systemd/system/zihi.service', { stdio: 'inherit' });
|
|
340
|
+
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
|
|
341
|
+
console.log('[zihi] systemd 服务已卸载');
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.error('[zihi] 卸载失败:', e.message);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
133
347
|
} else if (cmd === 'exclusive') {
|
|
134
348
|
// 独占模式开关
|
|
135
349
|
const { existsSync } = await import('fs');
|
|
@@ -594,10 +808,17 @@ SSH 开发:
|
|
|
594
808
|
`);
|
|
595
809
|
|
|
596
810
|
} else if (cmd === '-d' || cmd === '--daemon' || cmd === 'daemon') {
|
|
597
|
-
startDaemon();
|
|
598
|
-
|
|
811
|
+
await startDaemon();
|
|
812
|
+
// startDaemon 内部会在检测到已运行时 exit(0);
|
|
813
|
+
// 正常启动路径由 poll 定时器结束进程。
|
|
599
814
|
|
|
600
815
|
} else {
|
|
601
|
-
// 默认 / 'start'
|
|
816
|
+
// 默认 / 'start':前台运行前也要做唯一性校验
|
|
817
|
+
const running = await verifyRunning();
|
|
818
|
+
if (running) {
|
|
819
|
+
printRunningInfo(running);
|
|
820
|
+
process.exit(0);
|
|
821
|
+
}
|
|
822
|
+
if (readPidInfo()) { try { unlinkSync(pidFile); } catch {} }
|
|
602
823
|
await import('../dist/index.js');
|
|
603
824
|
}
|