rexp-web 0.1.0 → 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
@@ -4,7 +4,9 @@
4
4
 
5
5
  ## 安装与打开
6
6
 
7
- 推荐通过 Handfree SSH-MCP 的 `plugin-list` 发现 `rexp-web`,让 Agent 安装。
7
+ 正式安装包为 [rexp-web](https://www.npmjs.com/package/rexp-web),从官方 npm 源获取。
8
+ 推荐通过 Handfree SSH-MCP 的 `plugin-list` 发现 `rexp-web`,让 Agent 使用 `plugin-activate install` 安装;
9
+ 以后使用 `plugin-activate update` 更新。
8
10
  安装后 Agent 读取本包的 `web-operations` skill,询问是否开启开机自启动,
9
11
  保存宿主的 SSH 配置路径,再启动并打开控制台。首次安装不会自动修改系统启动项。
10
12
  请将打开的地址加入浏览器书签,下次直接访问。
@@ -30,7 +32,7 @@ rexp-web stop
30
32
 
31
33
  通过插件接入时,这些路径由宿主传入,`configure` 自动保存;无需用户手写。
32
34
  用户路径、SSH 凭据和连接配置不会写进 npm 包。没有 YAML 覆盖时沿用 SSH-MCP 的 OpenSSH 配置。
33
- 运行时不会再依赖相邻源码仓库。网页服务按需持有自己的 MCP 子进程,独立于 Agent 会话。
35
+ 安装包自带构建好的页面,无需自行构建。网页服务按需持有自己的 MCP 子进程,独立于 Agent 会话。
34
36
 
35
37
  默认访问地址为 `http://127.0.0.1:5173/`,服务只监听本机。
36
38
  `open` 自动复用已运行服务;关闭浏览器不停止服务,也不关闭 Agent 的通道或远端实验。
@@ -56,18 +58,6 @@ macOS 使用用户 LaunchAgent,Windows 使用用户启动目录,Linux 桌面
56
58
  设置位于 `~/.config/rexp-hub/web.json`,状态与日志位于 `~/.cache/rexp-hub/web/`。
57
59
  环境变量 `HANDFREE_MCP_DIR`、`HANDFREE_MCP_CONFIG`、`HANDFREE_PLUGINS_CONFIG` 可覆盖连接路径。
58
60
 
59
- ## 源码开发
60
-
61
- ```bash
62
- npm install
63
- npm run build
64
- npm start
65
- ```
66
-
67
- 发行包自带构建好的页面,普通启动不会安装依赖或执行构建。
68
- 源码前台开发使用 `npm run dev`,按 Ctrl+C 退出;后台停止用 `npm run web:stop`。
69
- 本地工具箱可以作为并行入口,工具本身支持独立启动。
70
-
71
61
  ## 当前数据
72
62
 
73
63
  - 节点列表来自 SSH-MCP `list-servers`,页面可见时每 3 秒轻量刷新,切回页面立即刷新,后端列表缓存仅 1 秒。新加入的 SSH 节点无需安装 REXP 即可显示;尚未探测时显示“检测中”。主机状态约每 30 秒在后台探测,不阻塞节点列表,也不因单台主机响应慢而延迟发现其他节点。
@@ -99,7 +89,7 @@ npm start
99
89
  单个节点的读取错误只在选中该节点时显示简短提示;原始错误可展开“技术详情”,默认收起。
100
90
  其他节点的查询和实验完成通知继续工作,安装完成后按正常刷新自动恢复。
101
91
 
102
- 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 程序位置和启动参数路径属于启动设置,与节点/插件内容更新不同。
103
93
 
104
94
  ## Ray Tune 参数搜索面板
105
95
 
@@ -137,16 +127,6 @@ trial 表格分页;图表大于 500 个点时抽样显示并标明数量,完
137
127
  这些图是对现有快照的本机分析,不启动远端 Ray/Optuna Dashboard,不额外扫描日志或轮询 GPU。
138
128
  参数关联不等于参数重要性或因果;最终效果图排除失败/未完成记录。大量绘图数据抽样并标注,表格不抽样。
139
129
 
140
- 独立查看 UI 演示(仅本机生成的示例数据,完全不连接 SSH):
141
-
142
- ```bash
143
- npm run build
144
- node scripts/preview-ui.mjs
145
- ```
146
-
147
- 访问 `http://127.0.0.1:5186/?view=tune`,在运行终端按 Ctrl+C 退出。
148
- 演示与正式 `5173` 服务独立;无需安装平台、Ray 或 Optuna。
149
-
150
130
  指标在列表、选择器、图表及详情统一显示简短中文。搜索配置的 `metric_labels` 优先,
151
131
  旧记录中的常用指标使用固定翻译;无法确定含义的自定义指标显示“指标 N”,不猜测。
152
132
  悬停标签查看原始字段名,详情可展开原始指标和完整精度;排序与计算仍使用原始字段。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rexp-web",
3
- "version": "0.1.0",
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
+ }