rexp-web 0.1.1 → 0.1.3
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 +3 -1
- package/package.json +1 -1
- package/scripts/web.mjs +9 -1
- package/server/bridge.mjs +5 -2
- package/server/node-status.mjs +61 -0
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ macOS 使用用户 LaunchAgent,Windows 使用用户启动目录,Linux 桌面
|
|
|
89
89
|
单个节点的读取错误只在选中该节点时显示简短提示;原始错误可展开“技术详情”,默认收起。
|
|
90
90
|
其他节点的查询和实验完成通知继续工作,安装完成后按正常刷新自动恢复。
|
|
91
91
|
|
|
92
|
-
Windows
|
|
92
|
+
Windows 节点由 Web 在后台补充 PowerShell 状态探测,进程详情使用 CIM,不需要修改 SSH-MCP 核心,也不要求先安装 REXP。探测共用连接,每个节点最多约 30 秒查询一次,不阻塞服务器列表。采集不到的 CPU/用户信息显示未知,不用零代替。SSH 已连接但资源探测失败时,不会直接把节点标成离线。新增 SSH 节点由 Handfree 的文件监听加载,插件注册内容每次调用读取;网页下次正常刷新即可显示节点与新安装的 REXP,无需重启 Codex 或 Web。网页节点列表约 3 秒刷新、后端列表缓存约 1 秒。`web.json` 中 MCP 程序位置和启动参数路径属于启动设置,与节点/插件内容更新不同。
|
|
93
93
|
|
|
94
94
|
## Ray Tune 参数搜索面板
|
|
95
95
|
|
|
@@ -177,3 +177,5 @@ SSH 可连接与平台可用分别检测:未配置、未安装、未初始化
|
|
|
177
177
|
这是选中服务器上实验调度器的开关;右上角“Web 设置”管理的仍是本机网页服务登录自启动,两者独立。终端也可使用 `rexp autostart --enable/--disable`,以及 `rexp start` / `rexp stop` 手动启停。
|
|
178
178
|
|
|
179
179
|
参数搜索将控制器存活与实际执行分开展示:有尝试计算时显示“正在执行尝试”,否则提示等待调度/资源。尝试列表可筛选“等待中”,等待记录显示占用卡的其他 run;运行区计数与独立刷新的运行尝试列表保持同步。
|
|
180
|
+
|
|
181
|
+
异常退出后的恢复:`rexp-web start` 会通过本机健康接口确认服务身份,自动忽略断电或重启后遗留的 PID 记录;不会终止占用了旧 PID 的其他进程。macOS 登录启动日志保存在 `~/.cache/rexp-hub/web/service.log`,不要求后台服务打开桌面目录中的日志文件。登录启动仅在已开启时生效,可用 `rexp-web autostart status --json` 查看。
|
package/package.json
CHANGED
package/scripts/web.mjs
CHANGED
|
@@ -91,7 +91,15 @@ async function main() {
|
|
|
91
91
|
}
|
|
92
92
|
if (!['start','open'].includes(action)) throw new Error('用法:rexp-web start|open|stop|status|logs|configure|autostart|skills|version');
|
|
93
93
|
if (await matchingService(info)) { console.log(`Web 已在运行:http://127.0.0.1:${info.port}/`); if(action==='open') await openPage(info.port); return; }
|
|
94
|
-
|
|
94
|
+
// An old PID can belong to another process after a crash or reboot. It is
|
|
95
|
+
// only a hint: never signal it or block startup without HTTP identity proof.
|
|
96
|
+
const recordedService = info && Number.isInteger(info.port) ? await health(info.port) : null;
|
|
97
|
+
if (recordedService?.service === 'rexp-hub-web' && Number.isInteger(recordedService.pid)) {
|
|
98
|
+
fs.writeFileSync(pidFile, JSON.stringify({...info, pid: recordedService.pid}), {mode:0o600});
|
|
99
|
+
console.log(`Web 已在运行:http://127.0.0.1:${info.port}/`);
|
|
100
|
+
if(action==='open') await openPage(info.port);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
95
103
|
if (info) fs.rmSync(pidFile, { force: true });
|
|
96
104
|
const port = Number(process.env.REXP_WEB_PORT || 5173);
|
|
97
105
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("REXP_WEB_PORT 必须是有效端口");
|
package/server/bridge.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { npuQuery, parseNpuSnapshot } from "./accelerators.mjs";
|
|
|
8
8
|
import { RunMonitor } from "./run-monitor.mjs";
|
|
9
9
|
import { errorInfo } from "./errors.mjs";
|
|
10
10
|
import { createServerDirectory, serverState } from "./server-directory.mjs";
|
|
11
|
+
import { createNodeStatusEnricher } from "./node-status.mjs";
|
|
11
12
|
import {autostartStatus,setAutostart} from './autostart.mjs';
|
|
12
13
|
import {webPaths} from './web-paths.mjs';
|
|
13
14
|
|
|
@@ -40,6 +41,7 @@ const runtimeDir = path.join(os.homedir(), ".cache", "rexp-hub", "ssh-mcp");
|
|
|
40
41
|
const outputLogDir = path.join(runtimeDir, ".handfree-output");
|
|
41
42
|
let clientPromise;
|
|
42
43
|
const serverDirectory = createServerDirectory({ invoke: (...args) => callTool(...args) });
|
|
44
|
+
const nodeStatus = createNodeStatusEnricher({ invoke: (...args) => callTool(...args) });
|
|
43
45
|
const gpuStreams = new Map();
|
|
44
46
|
const acceleratorFacts = new Map();
|
|
45
47
|
const npuSamples = new Map();
|
|
@@ -77,6 +79,7 @@ export async function closeBridge() {
|
|
|
77
79
|
const pending = clientPromise;
|
|
78
80
|
clientPromise = undefined;
|
|
79
81
|
serverDirectory.clear();
|
|
82
|
+
nodeStatus.clear();
|
|
80
83
|
acceleratorFacts.clear();
|
|
81
84
|
npuSamples.clear();
|
|
82
85
|
if (pending) {
|
|
@@ -114,7 +117,7 @@ async function mcpClient() {
|
|
|
114
117
|
await client.close().catch(() => {});
|
|
115
118
|
throw new Error(`SSH-MCP 启动失败:${startupError || error.message}`);
|
|
116
119
|
}
|
|
117
|
-
client.onclose = () => { clientPromise = undefined; serverDirectory.clear(); };
|
|
120
|
+
client.onclose = () => { clientPromise = undefined; serverDirectory.clear(); nodeStatus.clear(); };
|
|
118
121
|
return client;
|
|
119
122
|
})().catch((error) => { clientPromise = undefined; throw error; });
|
|
120
123
|
}
|
|
@@ -308,7 +311,7 @@ export function enrichWindowsProcesses(text, processes) {
|
|
|
308
311
|
|
|
309
312
|
async function listServers(force = false) {
|
|
310
313
|
const raw = await serverDirectory.list(force);
|
|
311
|
-
const items = raw.filter((item) => item.enabled).map((item) => {
|
|
314
|
+
const items = nodeStatus.enrich(raw.filter((item) => item.enabled)).map((item) => {
|
|
312
315
|
const gpus = item.status?.gpus ?? [];
|
|
313
316
|
const busy = gpus.some((gpu) => (numberOrUndefined(String(gpu.usage).replace("%", "")) ?? 0) >= 20);
|
|
314
317
|
return {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Web-side platform compatibility; SSH-MCP core remains independent of this UI.
|
|
2
|
+
const windowsProbe = `
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
5
|
+
$info = [ordered]@{ reachable = $true; hostname = $env:COMPUTERNAME; osName = 'Windows'; lastUpdated = [DateTime]::UtcNow.ToString('o') }
|
|
6
|
+
try {
|
|
7
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
8
|
+
$info.osVersion = $os.Caption
|
|
9
|
+
$info.kernelVersion = $os.Version
|
|
10
|
+
$info.memory = @{ free = ([math]::Round($os.FreePhysicalMemory/1MB, 1).ToString() + 'G'); total = ([math]::Round($os.TotalVisibleMemorySize/1MB, 1).ToString() + 'G') }
|
|
11
|
+
} catch {}
|
|
12
|
+
try {
|
|
13
|
+
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
14
|
+
$info.cpu = @{ name = $cpu.Name }
|
|
15
|
+
if ($null -ne $cpu.LoadPercentage) { $info.cpu.usage = ($cpu.LoadPercentage.ToString() + '%') }
|
|
16
|
+
} catch {}
|
|
17
|
+
try {
|
|
18
|
+
if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) {
|
|
19
|
+
$rows = @(& nvidia-smi --query-gpu=name,utilization.gpu --format=csv,noheader,nounits)
|
|
20
|
+
if ($LASTEXITCODE -eq 0) {
|
|
21
|
+
$info.gpus = @($rows | ForEach-Object { $p = $_ -split ','; @{ name = $p[0].Trim(); usage = ($p[1].Trim() + '%') } })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
$info | ConvertTo-Json -Depth 5 -Compress
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export function windowsStatusCommand() {
|
|
29
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(windowsProbe, 'utf16le').toString('base64')}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createNodeStatusEnricher({ invoke, now = Date.now, intervalMs = 30_000 }) {
|
|
33
|
+
const cache = new Map();
|
|
34
|
+
const keyFor = item => JSON.stringify([item.name, item.host, item.port, item.username]);
|
|
35
|
+
return {
|
|
36
|
+
enrich(items) {
|
|
37
|
+
const keys = new Set(items.map(keyFor));
|
|
38
|
+
for (const key of cache.keys()) if (!keys.has(key)) cache.delete(key);
|
|
39
|
+
return items.map(item => {
|
|
40
|
+
if (!item.connected || item.status?.osName || item.status?.reachable !== false) return item;
|
|
41
|
+
const key = keyFor(item);
|
|
42
|
+
let entry = cache.get(key);
|
|
43
|
+
if (!entry?.pending && (!entry || now() - entry.checkedAt >= intervalMs)) {
|
|
44
|
+
entry = { checkedAt: now(), pending: true, status: entry?.status };
|
|
45
|
+
cache.set(key, entry);
|
|
46
|
+
const current = entry;
|
|
47
|
+
Promise.resolve().then(() => invoke('execute-command', {
|
|
48
|
+
connectionName: item.name, cmdString: windowsStatusCommand(), stream: false, timeout: 10_000,
|
|
49
|
+
}, 15_000)).then(text => {
|
|
50
|
+
const status = JSON.parse(text.replace(/^\uFEFF/u, ''));
|
|
51
|
+
if (status?.osName !== 'Windows' || typeof status.hostname !== 'string') throw new Error('Unrecognized Windows status');
|
|
52
|
+
if (cache.get(key) === current) current.status = status;
|
|
53
|
+
}).catch(() => { /* Failed enrichment never marks a connected node offline. */ })
|
|
54
|
+
.finally(() => { current.pending = false; });
|
|
55
|
+
}
|
|
56
|
+
return entry?.status ? { ...item, status: { ...item.status, ...entry.status } } : item;
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
clear() { cache.clear(); },
|
|
60
|
+
};
|
|
61
|
+
}
|