diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml
deleted file mode 100644
index f7edbb2..0000000
--- a/.github/workflows/release-macos.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-name: Release macOS Desktop
-
-on:
- workflow_dispatch:
- inputs:
- release_tag:
- description: GitHub Release tag to upload assets to
- required: true
- default: v4.0.0
- upload_assets:
- description: Upload generated DMG files to the release
- required: true
- type: boolean
- default: true
-
-permissions:
- contents: write
-
-jobs:
- build-macos:
- name: macOS ${{ matrix.arch }}
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - arch: x64
- os: macos-13
- - arch: arm64
- os: macos-14
-
- steps:
- - name: Checkout source
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: npm
-
- - name: Install root dependencies
- run: npm ci
- env:
- npm_config_arch: ${{ matrix.arch }}
-
- - name: Build macOS desktop package
- run: npm run release:desktop:mac -- --arch=${{ matrix.arch }}
- env:
- CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- npm_config_arch: ${{ matrix.arch }}
-
- - name: Upload workflow artifact
- uses: actions/upload-artifact@v4
- with:
- name: 1Shell-mac-${{ matrix.arch }}
- path: release/desktop-mac/1Shell-v*-mac-${{ matrix.arch }}.dmg
- if-no-files-found: error
-
- - name: Upload to GitHub Release
- if: ${{ inputs.upload_assets }}
- env:
- GH_TOKEN: ${{ github.token }}
- RELEASE_TAG: ${{ inputs.release_tag }}
- run: gh release upload "$RELEASE_TAG" release/desktop-mac/1Shell-v*-mac-${{ matrix.arch }}.dmg --repo "$GITHUB_REPOSITORY" --clobber
diff --git a/desktop-start.bat b/desktop-start.bat
deleted file mode 100644
index 3b09817..0000000
--- a/desktop-start.bat
+++ /dev/null
@@ -1,14 +0,0 @@
-@echo off
-chcp 65001 >nul 2>&1
-setlocal
-cd /d "%~dp0"
-title 1Shell Desktop
-
-if exist "%~dp0node\npm.cmd" (
- set "NPM_CMD=%~dp0node\npm.cmd"
-) else (
- set "NPM_CMD=npm"
-)
-
-"%NPM_CMD%" run desktop:dev
-if errorlevel 1 pause
diff --git a/electron/icon.ico b/electron/icon.ico
deleted file mode 100644
index d198416..0000000
Binary files a/electron/icon.ico and /dev/null differ
diff --git a/electron/main.js b/electron/main.js
deleted file mode 100644
index abe5f81..0000000
--- a/electron/main.js
+++ /dev/null
@@ -1,397 +0,0 @@
-'use strict';
-
-const crypto = require('crypto');
-const fs = require('fs');
-const http = require('http');
-const net = require('net');
-const path = require('path');
-const { spawn } = require('child_process');
-const { app, BrowserWindow, Menu, Tray, nativeImage, shell, dialog, ipcMain } = require('electron');
-
-app.setName('1Shell');
-Menu.setApplicationMenu(null);
-
-let mainWindow = null;
-let tray = null;
-let backendProcess = null;
-let backendRoot = null;
-let backendPort = 3301;
-let isQuitting = false;
-
-function mainLog(message) {
- try {
- const logPath = path.join(app.getPath('userData'), 'desktop-main.log');
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
- fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`, 'utf8');
- } catch {
- // Ignore early logging failures.
- }
-}
-
-function randomSecret(bytes = 24) {
- return crypto.randomBytes(bytes).toString('base64url');
-}
-
-function resolveBackendRoot() {
- if (app.isPackaged) return path.join(process.resourcesPath, 'backend');
- return path.resolve(__dirname, '..');
-}
-
-function getLogPath() {
- return path.join(backendRoot, 'logs', 'desktop.log');
-}
-
-function appendLog(line) {
- try {
- fs.mkdirSync(path.dirname(getLogPath()), { recursive: true });
- fs.appendFileSync(getLogPath(), line, 'utf8');
- } catch {
- // Logging must never prevent app startup.
- }
-}
-
-function findAvailablePort(startPort) {
- return new Promise((resolve) => {
- function probe(port) {
- const server = net.createServer();
- server.once('error', () => probe(port + 1));
- server.once('listening', () => {
- server.close(() => resolve(port));
- });
- server.listen(port, '127.0.0.1');
- }
- probe(startPort);
- });
-}
-
-function readEnvValue(name) {
- const envPath = path.join(backendRoot, '.env');
- try {
- const env = fs.readFileSync(envPath, 'utf8');
- const match = env.match(new RegExp(`^${name}=(.*)$`, 'm'));
- return match ? match[1].trim() : '';
- } catch {
- return '';
- }
-}
-
-async function ensureEnv() {
- const envPath = path.join(backendRoot, '.env');
- if (fs.existsSync(envPath)) {
- const configuredPort = Number(readEnvValue('PORT'));
- backendPort = Number.isInteger(configuredPort) && configuredPort > 0 ? configuredPort : 3301;
- return;
- }
-
- backendPort = await findAvailablePort(3301);
- const content = [
- 'OPENAI_API_BASE=https://api.openai.com/v1',
- 'OPENAI_API_KEY=',
- 'OPENAI_MODEL=gpt-4o',
- 'APP_LOGIN_USERNAME=',
- 'APP_LOGIN_PASSWORD=',
- `APP_SECRET=${randomSecret(32)}`,
- 'APP_SESSION_TTL_HOURS=12',
- `PORT=${backendPort}`,
- `BRIDGE_TOKEN=${randomSecret(32)}`,
- '',
- ].join('\n');
-
- fs.writeFileSync(envPath, content, 'utf8');
-}
-
-function backendCommand(serverPath) {
- const bundledNode = path.join(backendRoot, 'node', 'node.exe');
- if (process.platform === 'win32' && fs.existsSync(bundledNode)) {
- return { executable: bundledNode, args: [serverPath], env: {} };
- }
- return {
- executable: process.execPath,
- args: [serverPath],
- env: { ELECTRON_RUN_AS_NODE: '1' },
- };
-}
-
-function startBackend() {
- if (backendProcess && !backendProcess.killed) return;
-
- const serverPath = path.join(backendRoot, 'server.js');
- const command = backendCommand(serverPath);
- const env = {
- ...process.env,
- ...command.env,
- NODE_PATH: path.join(backendRoot, 'deps'),
- PORT: String(backendPort),
- ELECTRON_DESKTOP: '1',
- ONESHELL_DESKTOP: '1',
- };
-
- appendLog(`\n[desktop] starting backend: ${new Date().toISOString()}\n`);
- backendProcess = spawn(command.executable, command.args, {
- cwd: backendRoot,
- env,
- windowsHide: true,
- });
-
- backendProcess.stdout.on('data', chunk => appendLog(chunk.toString()));
- backendProcess.stderr.on('data', chunk => appendLog(chunk.toString()));
- backendProcess.on('exit', (code, signal) => {
- appendLog(`[desktop] backend exited code=${code} signal=${signal}\n`);
- backendProcess = null;
- if (!isQuitting && mainWindow) showErrorPage('本地服务已停止。', '请从托盘菜单选择“重启服务”,或退出后重新打开 1Shell。');
- });
-}
-
-function stopBackend() {
- if (!backendProcess || backendProcess.killed) return;
- const pid = backendProcess.pid;
- backendProcess.kill('SIGINT');
- if (process.platform === 'win32' && pid) {
- setTimeout(() => {
- if (backendProcess) spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true });
- }, 1500).unref();
- }
-}
-
-function waitForHealth(deadlineMs = 30000) {
- const startedAt = Date.now();
- const healthUrl = `http://127.0.0.1:${backendPort}/api/health`;
-
- return new Promise((resolve, reject) => {
- function retry() {
- if (Date.now() - startedAt > deadlineMs) {
- reject(new Error(`Backend did not become ready at ${healthUrl}`));
- return;
- }
- setTimeout(probe, 500);
- }
-
- function probe() {
- const req = http.get(healthUrl, res => {
- res.resume();
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 500) {
- resolve();
- return;
- }
- retry();
- });
- req.on('error', retry);
- req.setTimeout(1000, () => {
- req.destroy();
- retry();
- });
- }
-
- probe();
- });
-}
-
-function escapeHtml(value) {
- return String(value)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
-function loadingHtml(message, detail = '') {
- const safeMessage = escapeHtml(message);
- const safeDetail = escapeHtml(detail);
- return `data:text/html;charset=utf-8,${encodeURIComponent(`
-
1Shell1_
1Shell
${safeMessage}
${safeDetail ? `${safeDetail}
` : ''}` )}`;
-}
-
-function showErrorPage(message, detail = '') {
- if (!mainWindow) return;
- mainWindow.loadURL(loadingHtml(message, detail));
-}
-
-function getDesktopSettingsPath() {
- return path.join(backendRoot, 'data', 'desktop-settings.json');
-}
-
-function readDesktopPreferences() {
- try {
- return JSON.parse(fs.readFileSync(getDesktopSettingsPath(), 'utf8'));
- } catch {
- return {};
- }
-}
-
-function writeDesktopPreferences(preferences) {
- const settingsPath = getDesktopSettingsPath();
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
- fs.writeFileSync(settingsPath, JSON.stringify(preferences, null, 2), 'utf8');
-}
-
-function getLoginItemOptions() {
- return process.platform === 'win32' ? { path: process.execPath } : {};
-}
-
-function desktopSettings() {
- const preferences = readDesktopPreferences();
- const loginItemSettings = app.getLoginItemSettings(getLoginItemOptions());
- return {
- isDesktop: true,
- runtime: 'electron',
- backgroundEnabled: preferences.backgroundEnabled === true,
- autostartEnabled: Boolean(loginItemSettings.openAtLogin),
- autostartAvailable: process.platform === 'win32' || process.platform === 'darwin',
- };
-}
-
-function setDesktopBackgroundEnabled(enabled) {
- writeDesktopPreferences({
- ...readDesktopPreferences(),
- backgroundEnabled: enabled,
- });
- return desktopSettings();
-}
-
-function setDesktopAutostartEnabled(enabled) {
- if (process.platform !== 'win32' && process.platform !== 'darwin') {
- throw new Error('当前系统不支持开机自启');
- }
- app.setLoginItemSettings({
- ...getLoginItemOptions(),
- openAtLogin: enabled,
- openAsHidden: false,
- });
- return desktopSettings();
-}
-
-function registerDesktopIpc() {
- ipcMain.handle('desktop:get-settings', () => desktopSettings());
- ipcMain.handle('desktop:set-background-enabled', (_event, enabled) => setDesktopBackgroundEnabled(Boolean(enabled)));
- ipcMain.handle('desktop:set-autostart-enabled', (_event, enabled) => setDesktopAutostartEnabled(Boolean(enabled)));
-}
-
-function iconPath() {
- const candidates = [
- path.join(backendRoot, 'frontend', 'dist', 'favicon.png'),
- path.join(backendRoot, 'public', 'favicon.ico'),
- ];
- return candidates.find(candidate => fs.existsSync(candidate)) || '';
-}
-
-function createWindow() {
- const icon = iconPath();
- mainWindow = new BrowserWindow({
- width: 1280,
- height: 860,
- minWidth: 960,
- minHeight: 640,
- show: false,
- title: '1Shell',
- autoHideMenuBar: true,
- icon: icon || undefined,
- webPreferences: {
- preload: path.join(__dirname, 'preload.js'),
- contextIsolation: true,
- nodeIntegration: false,
- sandbox: true,
- },
- });
-
- mainWindow.once('ready-to-show', () => mainWindow.show());
- mainWindow.on('close', (event) => {
- if (isQuitting) return;
- if (desktopSettings().backgroundEnabled && tray) {
- event.preventDefault();
- mainWindow.hide();
- }
- });
- mainWindow.on('closed', () => {
- mainWindow = null;
- if (!isQuitting) quitApp();
- });
-
- mainWindow.loadURL(loadingHtml('正在启动本地服务,请稍候...'));
-}
-
-function createTray() {
- const icon = iconPath();
- if (!icon) return;
-
- const image = nativeImage.createFromPath(icon);
- if (image.isEmpty()) return;
-
- tray = new Tray(image.resize({ width: 16, height: 16 }));
- tray.setToolTip('1Shell');
- tray.setContextMenu(Menu.buildFromTemplate([
- { label: '打开 1Shell', click: () => showMainWindow() },
- { label: '重启服务', click: () => restartBackend() },
- { label: '打开日志目录', click: () => shell.openPath(path.dirname(getLogPath())) },
- { type: 'separator' },
- { label: '退出', click: () => quitApp() },
- ]));
- tray.on('double-click', () => showMainWindow());
-}
-
-function showMainWindow() {
- if (!mainWindow) createWindow();
- mainWindow.show();
- mainWindow.focus();
-}
-
-async function loadApp() {
- try {
- await waitForHealth();
- await mainWindow.loadURL(`http://127.0.0.1:${backendPort}/app/?runtime=desktop`);
- } catch (error) {
- appendLog(`[desktop] ${error.stack || error.message}\n`);
- showErrorPage('本地服务启动失败。', '请打开托盘菜单中的日志目录,查看 desktop.log。');
- }
-}
-
-async function restartBackend() {
- stopBackend();
- await new Promise(resolve => setTimeout(resolve, 1200));
- startBackend();
- if (mainWindow) mainWindow.loadURL(loadingHtml('正在重启本地服务...'));
- await loadApp();
-}
-
-function quitApp() {
- isQuitting = true;
- stopBackend();
- app.quit();
-}
-
-const gotLock = app.requestSingleInstanceLock();
-if (!gotLock) {
- mainLog('single instance lock denied');
- app.quit();
-} else {
- mainLog('single instance lock acquired');
- app.on('second-instance', () => showMainWindow());
-
- app.whenReady().then(async () => {
- mainLog('app ready');
- registerDesktopIpc();
- backendRoot = resolveBackendRoot();
- mainLog(`backendRoot=${backendRoot}`);
- await ensureEnv();
- mainLog(`backendPort=${backendPort}`);
- createWindow();
- createTray();
- startBackend();
- await loadApp();
- }).catch(error => {
- mainLog(`startup failed: ${error.stack || error.message}`);
- dialog.showErrorBox('1Shell failed to start', error.stack || error.message);
- app.quit();
- });
-
- app.on('activate', () => showMainWindow());
- app.on('before-quit', () => {
- isQuitting = true;
- stopBackend();
- });
- app.on('window-all-closed', () => {
- if (!isQuitting) quitApp();
- });
-}
diff --git a/electron/preload.js b/electron/preload.js
deleted file mode 100644
index 0bb8632..0000000
--- a/electron/preload.js
+++ /dev/null
@@ -1,12 +0,0 @@
-'use strict';
-
-const { contextBridge, ipcRenderer } = require('electron');
-
-contextBridge.exposeInMainWorld('__ONESHELL_RUNTIME__', 'desktop');
-contextBridge.exposeInMainWorld('oneShellDesktop', {
- runtime: 'desktop',
- platform: process.platform,
- getSettings: () => ipcRenderer.invoke('desktop:get-settings'),
- setAutostartEnabled: enabled => ipcRenderer.invoke('desktop:set-autostart-enabled', Boolean(enabled)),
- setBackgroundEnabled: enabled => ipcRenderer.invoke('desktop:set-background-enabled', Boolean(enabled)),
-});
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index b2a31bd..c11a2c8 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -10,8 +10,6 @@ import AppAiFab from './components/AppAiFab.vue';
import ConfirmModal from './components/ConfirmModal.vue';
import AppBackground from './components/AppBackground.vue';
import LoginScreen from './components/main/LoginScreen.vue';
-import SettingsModal from './components/main/SettingsModal.vue';
-import { isDesktopRuntime } from './utils/desktop';
interface AuthStatusResp {
enabled?: boolean;
@@ -23,14 +21,13 @@ const { requestJson } = useApiClient();
const bootstrapping = ref(true);
const bootstrapError = ref(null);
-const settingsOpen = ref(false);
function prefetchProbeSilently(force = false): void {
void prefetchProbePageState(requestJson, force).catch(() => undefined);
}
-const desktopRuntime = isDesktopRuntime();
-const needLogin = computed(() => !desktopRuntime && auth.enabled && !auth.authenticated);
+// 全局登录闸门:auth 启用且未登录 → 必须先登录
+const needLogin = computed(() => auth.enabled && !auth.authenticated);
async function bootstrapAuth(): Promise {
try {
@@ -76,7 +73,7 @@ onMounted(() => {
-
+
@@ -87,6 +84,5 @@ onMounted(() => {
-
diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue
index 9c89f03..2cc8b96 100644
--- a/frontend/src/components/AppSidebar.vue
+++ b/frontend/src/components/AppSidebar.vue
@@ -13,6 +13,7 @@ interface NavItem {
const navItems: NavItem[] = [
{ to: '/', label: '主页', icon: 'globe', title: '世界地图主页' },
{ to: '/console', label: '主控', icon: 'console', title: '主控台' },
+ { to: '/hosts', label: '主机', icon: 'server', title: 'VPS 仓库' },
{ to: '/programs', label: '程序', icon: 'cog', title: '长驻程序' },
{ to: '/scripts', label: '脚本', icon: 'terminal', title: '脚本库' },
{ to: '/skills', label: '仓库', icon: 'package', title: '技能仓库' },
@@ -22,8 +23,6 @@ const navItems: NavItem[] = [
{ to: '/audit', label: '审计', icon: 'clipboard', title: '审计日志' },
];
-const emit = defineEmits<{ 'open-settings': [] }>();
-
const isDark = ref(true);
const themeIcon = ref<'sun' | 'moon'>('sun');
@@ -81,15 +80,6 @@ onMounted(syncFromDom);
主题
-
diff --git a/frontend/src/components/main/SettingsModal.vue b/frontend/src/components/main/SettingsModal.vue
index 7511bb4..9ef822b 100644
--- a/frontend/src/components/main/SettingsModal.vue
+++ b/frontend/src/components/main/SettingsModal.vue
@@ -1,16 +1,9 @@