rexp-web 0.1.1 → 0.1.2
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 +1 -1
- package/package.json +1 -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
|
|
package/package.json
CHANGED
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
|
+
}
|