diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 5bdcffe..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,11 +0,0 @@ -[source.crates-io] -replace-with = "rsproxy-sparse" - -[source.rsproxy] -registry = "https://rsproxy.cn/crates.io-index" - -[source.rsproxy-sparse] -registry = "sparse+https://rsproxy.cn/index/" - -[net] -git-fetch-with-cli = true diff --git a/.gitignore b/.gitignore index cdc5509..8c13a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,16 @@ node_modules/ conpty.dll OpenConsole.exe +# 测试证据与界面截图(含真实机队 IP/主机名,不入库也不进发布包) +/artifacts +/app-*.png + # 系统 .DS_Store Thumbs.db + +# 本地构建用的 crates 镜像源(公开仓库不该替所有人改源,各人自备) +/.cargo/ + +# cargo wix 每次构建在包根生成的展开式 wxs(中间产物) +/*.wxs diff --git a/Cargo.toml b/Cargo.toml index 0ae545b..ea732a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "vps-switch" version = "0.1.0" edition = "2021" description = "Lightweight VPS manager and MCP server" +authors = ["weidu12123"] [dependencies] tokio = { version = "1.40", features = ["full"] } diff --git a/docs/plan.md b/docs/plan.md index 8fa0003..eaba037 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -25,7 +25,7 @@ | 1Shell 的痛点 | 根源 | vps-switch 的解法 | |---|---|---| | **定位散乱、体积臃肿(80k 行)** | 第一次做项目时误打误撞,把内置 AI 对话、Claude/Codex 驻留面板、技能库、脚本库、工作负载全塞了进来 | 砍掉所有内置 AI 与多余概念,回归纯粹的轻量级 VPS 管理工具 + 外部 MCP 接口 | -| **主机仓库信息滞后** | `osName` 等信息首次探测后成了死缓存(如 `lazy-gomami` 重装为 Ubuntu 后列表仍显示 Alpine 3.23) | SSH 连接建立时轻量静默刷新真实 OS/内核/架构信息,保持仓库数据鲜活 | +| **主机仓库信息滞后** | `osName` 等信息首次探测后成了死缓存(如某台机器重装为 Ubuntu 后列表仍显示 Alpine 3.23) | SSH 连接建立时轻量静默刷新真实 OS/内核/架构信息,保持仓库数据鲜活 | | **终端总有小 bug** | Node 仅做字节透传,服务端无终端状态机;切页丢屏、UTF-8 多字节截断乱码、resize 错位、不知命令何时结束 | 参考 Pebrel 技术栈:Rust 侧 `alacritty_terminal::Term` 持有权威网格状态 + 字节环重连恢复 + 随包 Windows ConPTY + OSC 133/7 真实命令边界与 exit code + WebSocket 纯二进制帧 | | **文件上传下载困难(尤其 Agent 调用时)** | 传文件被拆成 `start/append/finish_file_upload` 等 9 个工具,逼大模型在上下文里手搓 base64 分块,慢且极易断 | 做独立的 Rust 流式传输引擎(接收 `本机路径 ↔ 远端路径`),内部自动流式分片、断点续传、SHA256 校验;UI 队列与 MCP 共用同一条链路 | | **探针过重、后台刷屏** | 塞了 Agentless / Go Agent / Relay 级联三种模式与复杂告警,40,826 次 SSH 轮询占了 76% 活动,告警刷出 1.1 万条 | 重做轻量探针:复用 SSH 连接或按需/低频采样(CPU、内存、磁盘、实时网速/流量、基础负载),打开面板时实时刷新,后台静默不折腾 | @@ -78,7 +78,7 @@ - **核心指标**:在线状态与连接延迟、系统运行时长(Uptime)、CPU 使用率与负载(Load)、内存与 Swap、磁盘占用、实时上下行网速与累计流量、关键端口/进程概览。 - **轻量采集机制**:优先复用已建立的 SSH 连接或采用极低开销的单次合并脚本采样;**按需高频、后台低频/静默**(人打开探针页或选中主机时实时刷新,平时不疯狂建连刷日志)。 -- **兼容 Linux 与 Windows**:Linux(Debian/Ubuntu/Alpine)读取 `/proc` 与系统指标,Windows(`yun-computer`)走轻量性能计数采集。 +- **兼容 Linux 与 Windows**:Linux(Debian/Ubuntu/Alpine)读取 `/proc` 与系统指标,Windows 走轻量性能计数采集。 ### 板块六:工具化(MCP Server + 安全护栏) diff --git a/frontend/src/components/HostModal.vue b/frontend/src/components/HostModal.vue index e3175ed..51aea69 100644 --- a/frontend/src/components/HostModal.vue +++ b/frontend/src/components/HostModal.vue @@ -178,7 +178,7 @@ function handleSubmit() { diff --git a/installer.iss b/installer.iss new file mode 100644 index 0000000..25da397 --- /dev/null +++ b/installer.iss @@ -0,0 +1,65 @@ +; Inno Setup Script for VPS Switch +; 编译为标准的 Windows Setup.exe 安装向导程序 + +#define MyAppName "VPS Switch" +#define MyAppVersion "0.1.0" +#define MyAppPublisher "weidu12123" +#define MyAppURL "https://github.com/weidu12123/vps-switch" +#define MyAppExeName "vps-switch.exe" + +[Setup] +; 基础信息 +AppId={{1357D496-DC42-4DA5-A3E0-76B6F7F778BC} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} + +; 安装路径:64 位 Program Files +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes + +; 图标与视觉 +SetupIconFile=assets\app.ico +UninstallDisplayIcon={app}\{#MyAppExeName} + +; 权限:默认请求管理员(装入 Program Files),支持所有用户 +PrivilegesRequired=admin +PrivilegesRequiredOverridesAllowed=dialog + +; 压缩算法 +Compression=lzma2/max +SolidCompression=yes +ArchitecturesInstallIn64BitMode=x64compatible +ArchitecturesAllowed=x64compatible + +; 产物输出路径与名称 +OutputDir=target\dist\packages +OutputBaseFilename=vps-switch-0.1.0-windows-x64-setup + +; 卸载清理:绝不删除 %APPDATA%\vps-switch 数据目录 +Uninstallable=yes +CreateUninstallRegKey=yes + +[Languages] +Name: "chinesesimp"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +; 核心程序与随包 Windows ConPTY +Source: "target\dist\release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "conpty.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "OpenConsole.exe"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/src/db.rs b/src/db.rs index 2f342df..18db331 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1208,21 +1208,16 @@ fn generate_api_key() -> String { ) } +/// 1Shell 的数据库只认它在系统里的正规位置:`%APPDATA%\1Shell\data\1shell.db`。 +/// 这里曾经挂过一条指向某个开发机绝对路径的回退,既把构建机的目录结构带进了发布的 exe, +/// 又会在 APPDATA 那份不在时静默读到一份陈旧副本——两者都不要。 fn find_1shell_db_path() -> Option { - if let Ok(appdata) = std::env::var("APPDATA") { - let candidate = PathBuf::from(appdata) - .join("1Shell") - .join("data") - .join("1shell.db"); - if candidate.exists() { - return Some(candidate); - } - } - let fallback = PathBuf::from(r"E:\1shell4.7\1shell-4.7\data\1shell.db"); - if fallback.exists() { - return Some(fallback); - } - None + let appdata = std::env::var("APPDATA").ok()?; + let candidate = PathBuf::from(appdata) + .join("1Shell") + .join("data") + .join("1shell.db"); + candidate.exists().then_some(candidate) } fn normalize_links(links: Vec) -> Vec { diff --git a/src/inspector.rs b/src/inspector.rs index 2032821..f5a2994 100644 --- a/src/inspector.rs +++ b/src/inspector.rs @@ -261,7 +261,7 @@ impl InspectorEngine { Ok(s) => s, Err(err) => { // 连不上要分两种情况:**超时**(不确定,可能只是线路慢)与**拒绝/认证失败** - // (确定连不上)。前者若也判离线,慢线路(zouter/lazy-* 这类)会长期误红, + // (确定连不上)。前者若也判离线,慢线路(跨境高延迟那类)会长期误红, // 所以记成 unknown 交给界面用灰灯表示"这次没判定出来",绝不冒充"离线"。 let msg = format!("{err:#}"); let status = if connect_error_is_timeout(&msg) { @@ -329,7 +329,7 @@ impl InspectorEngine { // 走到这里说明 SSH **已经连上**了(get_or_connect 成功)——这台机器是可连的。 // 探针跑不出来只代表"这次没采到数据",不该把灯判成离线:慢/抖线路上 channel // 中途掉一次(例如回退的 Windows 探针返回 Disconnected)就会让一台能连的主机 - // 常年标红(zouter 已实测复现)。所以保持在线,把采集失败原因写进 last_error。 + // 常年标红(真机实测复现过)。所以保持在线,把采集失败原因写进 last_error。 if exec_errored { // exec 层面出过错,池里这条连接不再可信,回收它下次重建 pool.invalidate_host(host_id).await; diff --git a/src/mcp.rs b/src/mcp.rs index da5dfc2..7d2ff86 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -834,7 +834,7 @@ pub fn mcp_tool_definitions() -> Value { "properties": { "host": { "type": "string", - "description": "目标主机名(如 'vip-hk'、'lazy-gomami'、'本机')或 IP" + "description": "目标主机名(如 'web-1'、'db-2'、'本机')或 IP" }, "hosts": { "type": "array", @@ -846,13 +846,13 @@ pub fn mcp_tool_definitions() -> Value { }, { "name": "vps_exec", - "description": "在一台或多台 VPS 上并发执行非交互式命令(直接传主机名如 'vip-hk')。若在 wait_seconds 内完成则同步返回 stdout/stderr/exitCode(ok=true);若超时未完则返回 state=running、ok=false 与 taskId(表示未完成、不是失败),此时若需要结果,用**一次** vps_task(action:'get', task_id, wait_seconds:N) 等待其结束即可,不要反复查询;若检测到命令卡在 [Y/n] 等交互提问超过 2 秒会自动早退提醒改用 vps_terminal。命令会先过本机命令黑名单,命中不可逆灾难(如 rm -rf /、mkfs、格式化盘)即直接拒绝执行、不产生任何输出——名单文件是 data/deny-list.txt,被拦时不要换写法重试,直接告诉用户哪条被拦。", + "description": "在一台或多台 VPS 上并发执行非交互式命令(直接传主机名如 'web-1')。若在 wait_seconds 内完成则同步返回 stdout/stderr/exitCode(ok=true);若超时未完则返回 state=running、ok=false 与 taskId(表示未完成、不是失败),此时若需要结果,用**一次** vps_task(action:'get', task_id, wait_seconds:N) 等待其结束即可,不要反复查询;若检测到命令卡在 [Y/n] 等交互提问超过 2 秒会自动早退提醒改用 vps_terminal。命令会先过本机命令黑名单,命中不可逆灾难(如 rm -rf /、mkfs、格式化盘)即直接拒绝执行、不产生任何输出——名单文件是 data/deny-list.txt,被拦时不要换写法重试,直接告诉用户哪条被拦。", "inputSchema": { "type": "object", "properties": { "host": { "type": "string", - "description": "单台目标主机名(如 'vip-hk'、'lazy-gomami'、'本机')或 IP" + "description": "单台目标主机名(如 'web-1'、'db-2'、'本机')或 IP" }, "hosts": { "type": "array", @@ -879,7 +879,7 @@ pub fn mcp_tool_definitions() -> Value { "properties": { "host": { "type": "string", - "description": "目标主机名(如 'vip-hk'、'lazy-gomami'、'本机')" + "description": "目标主机名(如 'web-1'、'db-2'、'本机')" }, "action": { "type": "string", @@ -926,7 +926,7 @@ pub fn mcp_tool_definitions() -> Value { "properties": { "host": { "type": "string", - "description": "目标主机名(如 'vip-hk'、'本机')" + "description": "目标主机名(如 'web-1'、'本机')" }, "action": { "type": "string", @@ -1297,7 +1297,7 @@ async fn tool_vps_list(state: &Arc, args: Value) -> Result { Ok(json!({ "ok": true, - "note": "所有工具均支持直接传 name(如 'vip-hk'、'lazy-gomami'、'本机')定位主机,无需使用内部 ID。", + "note": "所有工具均支持直接传 name(如 'web-1'、'db-2'、'本机')定位主机,无需使用内部 ID。", "total": out.len(), "hosts": out, })) @@ -1399,7 +1399,7 @@ async fn tool_vps_exec( } if host_queries.is_empty() { return Err(anyhow!( - "请提供 host 或 hosts 参数(直接传主机名,如 'vip-hk')" + "请提供 host 或 hosts 参数(直接传主机名,如 'web-1')" )); } diff --git a/vps-switch.service b/vps-switch.service new file mode 100644 index 0000000..dc056bc --- /dev/null +++ b/vps-switch.service @@ -0,0 +1,20 @@ +[Unit] +Description=VPS Switch - Lightweight VPS manager and MCP server +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/vps-switch +ExecStart=/opt/vps-switch/vps-switch +Restart=always +RestartSec=5 +Environment=PORT=18800 +Environment=HOST=0.0.0.0 +Environment=DATA_DIR=/opt/vps-switch/data + +# 文件句柄限制(满足高并发 SSH 与终端) +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target diff --git a/wix/main.wxs b/wix/main.wxs new file mode 100644 index 0000000..123f456 --- /dev/null +++ b/wix/main.wxs @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + + + + +