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 +5 -25
- package/package.json +1 -1
- package/server/bridge.mjs +5 -2
- package/server/node-status.mjs +61 -0
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## 安装与打开
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
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
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
|
+
}
|