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
+
+
+
+
+