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 CHANGED
@@ -89,7 +89,7 @@ macOS 使用用户 LaunchAgent,Windows 使用用户启动目录,Linux 桌面
89
89
  单个节点的读取错误只在选中该节点时显示简短提示;原始错误可展开“技术详情”,默认收起。
90
90
  其他节点的查询和实验完成通知继续工作,安装完成后按正常刷新自动恢复。
91
91
 
92
- Windows 节点状态由 Handfree 自动选择 PowerShell 探测,进程详情使用 CIM;采集不到的 CPU/用户信息显示未知,不用零代替。SSH 已连接但资源探测失败时,不会直接把节点标成离线。新增 SSH 节点由 Handfree 的文件监听加载,插件注册内容每次调用读取;网页下次正常刷新即可显示节点与新安装的 REXP,无需重启 Codex 或 Web。网页节点列表约 45 秒刷新、后端缓存最多 30 秒,因此更新并非每次都瞬间出现在页面上。`web.json` 中 MCP 程序位置和启动参数路径属于启动设置,与节点/插件内容更新不同。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rexp-web",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "start": "node scripts/web.mjs start",
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
+ }