Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0535c7208 | ||
|
|
c92ad8b096 | ||
|
|
e809de892d | ||
|
|
8095b2391e | ||
|
|
c529e6c51b | ||
|
|
bd46227e49 |
@@ -140,6 +140,43 @@ test('PM2 部署没有启用 cluster 或多实例参数', () => {
|
|||||||
assert.strictEqual(/pm2\s+start[^\n]*(?:\s-i\s|--instances)/.test(installer), false);
|
assert.strictEqual(/pm2\s+start[^\n]*(?:\s-i\s|--instances)/.test(installer), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('install.sh 可在最小化系统完成预检且不会吞掉 NodeSource 下载错误', () => {
|
||||||
|
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
|
||||||
|
assert.ok(installer.includes("/dev/tcp/git.workyai.cn/443"));
|
||||||
|
assert.strictEqual(installer.includes('ping -c 1 git.workyai.cn'), false);
|
||||||
|
assert.strictEqual(/^\s*clear\s*$/m.test(installer), false);
|
||||||
|
assert.ok(installer.includes('clear_screen'));
|
||||||
|
assert.ok(installer.includes('main "$@"'));
|
||||||
|
assert.ok(installer.includes('/etc/apt/sources.list.d/ubuntu.sources'));
|
||||||
|
for (const dependency of ['ca-certificates', 'iproute2', 'dnsutils', 'procps']) {
|
||||||
|
assert.ok(installer.includes(dependency), dependency);
|
||||||
|
}
|
||||||
|
assert.ok(installer.includes('download_and_run_setup_script'));
|
||||||
|
assert.strictEqual(/curl[^\n]+\|\s*(?:ba)?sh/.test(installer), false);
|
||||||
|
assert.ok(installer.includes('verify_nodejs_installation'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('install.sh 生成可用且受保护的生产环境配置', () => {
|
||||||
|
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
|
||||||
|
assert.ok(installer.includes('PUBLIC_BASE_URL=${PUBLIC_BASE_URL_VALUE}'));
|
||||||
|
assert.ok(installer.includes('ALLOWED_HOSTS=${ALLOWED_HOSTS_VALUE}'));
|
||||||
|
assert.ok(installer.includes('ALLOWED_ORIGINS=${ALLOWED_ORIGINS_VALUE}'));
|
||||||
|
assert.ok(installer.includes('chmod 600 "${PROJECT_DIR}/backend/.env"'));
|
||||||
|
assert.ok(installer.includes('dotenv_quote'));
|
||||||
|
assert.strictEqual(installer.includes('ALLOWED_ORIGINS_VALUE=""'), false);
|
||||||
|
assert.strictEqual(installer.includes('type_count++'), false);
|
||||||
|
assert.ok(installer.includes('/api/health'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('install.sh 只展示已实现的 SSL 方案且 Certbot 分支可达', () => {
|
||||||
|
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
|
||||||
|
assert.ok(installer.includes('deploy_certbot || ssl_fallback "1"'));
|
||||||
|
assert.ok(installer.includes('download_and_run_setup_script "https://get.acme.sh"'));
|
||||||
|
assert.strictEqual(installer.includes('证书申请功能开发中'), false);
|
||||||
|
assert.strictEqual(installer.includes('阿里云AccessKey ID'), false);
|
||||||
|
assert.strictEqual(installer.includes('腾讯云SecretId'), false);
|
||||||
|
});
|
||||||
|
|
||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
console.log('测试总结');
|
console.log('测试总结');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
|
|||||||
@@ -1802,7 +1802,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
|
|
||||||
<div class="navbar" v-if="isLoggedIn">
|
<nav class="navbar" v-if="isLoggedIn" aria-label="主导航">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<span class="brand-mark"><i class="fas fa-cloud"></i></span>
|
<span class="brand-mark"><i class="fas fa-cloud"></i></span>
|
||||||
<span class="brand-copy">
|
<span class="brand-copy">
|
||||||
@@ -1812,19 +1812,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="navbar-menu">
|
<div class="navbar-menu">
|
||||||
<div class="nav-section-label">工作台</div>
|
<div class="nav-section-label">工作台</div>
|
||||||
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'files'}" @click="switchView('files')">
|
<button v-if="user && !user.is_admin" type="button" class="nav-item" :class="{active: currentView === 'files'}" :aria-current="currentView === 'files' ? 'page' : null" @click="switchView('files')">
|
||||||
<i class="fas fa-folder"></i> 我的文件
|
<i class="fas fa-folder"></i><span class="nav-item-label">文件</span>
|
||||||
</div>
|
</button>
|
||||||
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'shares'}" @click="switchView('shares')">
|
<button v-if="user && !user.is_admin" type="button" class="nav-item" :class="{active: currentView === 'shares'}" :aria-current="currentView === 'shares' ? 'page' : null" @click="switchView('shares')">
|
||||||
<i class="fas fa-share-alt"></i> 我的分享
|
<i class="fas fa-share-alt"></i><span class="nav-item-label">分享</span>
|
||||||
</div>
|
</button>
|
||||||
<div v-if="user && user.is_admin" class="nav-item" :class="{active: currentView === 'admin'}" @click="switchView('admin')">
|
<button v-if="user && user.is_admin" type="button" class="nav-item" :class="{active: currentView === 'admin'}" :aria-current="currentView === 'admin' ? 'page' : null" @click="switchView('admin')">
|
||||||
<i class="fas fa-user-shield"></i> 管理员
|
<i class="fas fa-user-shield"></i><span class="nav-item-label">管理</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="nav-section-label nav-service-label">服务</div>
|
<div class="nav-section-label nav-service-label">服务</div>
|
||||||
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="switchView('settings')">
|
<button type="button" class="nav-item" :class="{active: currentView === 'settings'}" :aria-current="currentView === 'settings' ? 'page' : null" @click="switchView('settings')">
|
||||||
<i class="fas fa-cog"></i> 设置
|
<i class="fas fa-cog"></i><span class="nav-item-label">设置</span>
|
||||||
</div>
|
</button>
|
||||||
<button class="btn btn-secondary navbar-download-btn" :disabled="desktopClientDownloading" @click="downloadDesktopClient">
|
<button class="btn btn-secondary navbar-download-btn" :disabled="desktopClientDownloading" @click="downloadDesktopClient">
|
||||||
<i :class="desktopClientDownloading ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
|
<i :class="desktopClientDownloading ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
|
||||||
{{ desktopClientDownloading ? '准备中...' : '下载客户端' }}
|
{{ desktopClientDownloading ? '准备中...' : '下载客户端' }}
|
||||||
@@ -1837,15 +1837,15 @@
|
|||||||
<strong>{{ user.username }}</strong>
|
<strong>{{ user.username }}</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-danger navbar-logout-btn" @click="logout" title="退出登录">
|
<button class="btn btn-danger navbar-logout-btn" @click="logout" title="退出登录" aria-label="退出登录">
|
||||||
<i class="fas fa-power-off"></i> 退出
|
<i class="fas fa-power-off"></i><span class="nav-item-label">退出</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
<!-- 文件视图 -->
|
<!-- 文件视图 -->
|
||||||
<div v-if="isLoggedIn && currentView === 'files'" class="main-container">
|
<div v-if="isLoggedIn && currentView === 'files'" class="main-container files-main-container">
|
||||||
<div class="card files-view-card">
|
<div class="card files-view-card">
|
||||||
|
|
||||||
<header class="workspace-page-header files-page-header">
|
<header class="workspace-page-header files-page-header">
|
||||||
@@ -1909,19 +1909,22 @@
|
|||||||
<!-- 文件列表 -->
|
<!-- 文件列表 -->
|
||||||
<div v-else class="files-content-shell">
|
<div v-else class="files-content-shell">
|
||||||
<div class="files-command-bar">
|
<div class="files-command-bar">
|
||||||
<div class="files-command-primary">
|
<div class="files-command-primary files-command-primary-desktop">
|
||||||
<button class="btn btn-primary" @click="$refs.fileUploadInput.click()">
|
<button class="btn btn-primary" @click="$refs.fileUploadInput.click()">
|
||||||
<i class="fas fa-upload"></i> 上传文件
|
<i class="fas fa-upload"></i><span class="btn-label">上传文件</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" @click="showCreateFolderModal = true">
|
<button class="btn btn-secondary" @click="showCreateFolderModal = true">
|
||||||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
<i class="fas fa-folder-plus"></i><span class="btn-label">新建文件夹</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary mobile-file-create-trigger" @click="openMobileCreateSheet($event)" aria-label="新建或上传">
|
||||||
|
<i class="fas fa-plus"></i><span>新建</span>
|
||||||
|
</button>
|
||||||
<div class="view-toggle-group files-view-toggle" aria-label="文件视图切换">
|
<div class="view-toggle-group files-view-toggle" aria-label="文件视图切换">
|
||||||
<button class="btn" :class="fileViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'grid'" title="大图标视图">
|
<button class="btn" :class="fileViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="setFileViewMode('grid')" :aria-pressed="fileViewMode === 'grid'" aria-label="大图标视图" title="大图标视图">
|
||||||
<i class="fas fa-th-large"></i><span>大图标</span>
|
<i class="fas fa-th-large"></i><span>大图标</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn" :class="fileViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'list'" title="列表视图">
|
<button class="btn" :class="fileViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="setFileViewMode('list')" :aria-pressed="fileViewMode === 'list'" aria-label="列表视图" title="列表视图">
|
||||||
<i class="fas fa-list"></i><span>列表</span>
|
<i class="fas fa-list"></i><span>列表</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1932,14 +1935,15 @@
|
|||||||
<div class="files-search-field">
|
<div class="files-search-field">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="search"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
v-model="globalSearchKeyword"
|
v-model="globalSearchKeyword"
|
||||||
@input="triggerGlobalSearch"
|
@input="triggerGlobalSearch"
|
||||||
@focus="globalSearchVisible = !!globalSearchKeyword"
|
@focus="globalSearchVisible = !!globalSearchKeyword"
|
||||||
placeholder="搜索全部目录中的文件或文件夹">
|
aria-label="搜索文件或文件夹"
|
||||||
|
placeholder="搜索文件或文件夹">
|
||||||
</div>
|
</div>
|
||||||
<select class="form-input files-search-type" v-model="globalSearchType" @change="runGlobalSearch">
|
<select class="form-input files-search-type" v-model="globalSearchType" @change="runGlobalSearch" aria-label="搜索类型">
|
||||||
<option value="all">全部</option>
|
<option value="all">全部</option>
|
||||||
<option value="file">仅文件</option>
|
<option value="file">仅文件</option>
|
||||||
<option value="directory">仅文件夹</option>
|
<option value="directory">仅文件夹</option>
|
||||||
@@ -2114,6 +2118,7 @@
|
|||||||
@touchstart.stop
|
@touchstart.stop
|
||||||
@touchmove.stop
|
@touchmove.stop
|
||||||
@touchend.stop
|
@touchend.stop
|
||||||
|
aria-label="更多操作"
|
||||||
title="更多操作">
|
title="更多操作">
|
||||||
<i class="fas fa-ellipsis-h"></i>
|
<i class="fas fa-ellipsis-h"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -2510,7 +2515,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设置视图 -->
|
<!-- 设置视图 -->
|
||||||
<div v-if="isLoggedIn && currentView === 'settings'" class="main-container">
|
<div v-if="isLoggedIn && currentView === 'settings'" class="main-container settings-main-container">
|
||||||
<div class="card settings-page-card">
|
<div class="card settings-page-card">
|
||||||
<div class="settings-page-head">
|
<div class="settings-page-head">
|
||||||
<h2 class="settings-page-title">
|
<h2 class="settings-page-title">
|
||||||
@@ -3120,7 +3125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分享视图 -->
|
<!-- 分享视图 -->
|
||||||
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container">
|
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container shares-main-container">
|
||||||
<div class="card shares-page-card">
|
<div class="card shares-page-card">
|
||||||
<header class="workspace-page-header shares-page-head">
|
<header class="workspace-page-header shares-page-head">
|
||||||
<div class="workspace-page-heading">
|
<div class="workspace-page-heading">
|
||||||
@@ -3129,21 +3134,21 @@
|
|||||||
<p>集中查看分享状态、访问数据和文件直链。</p>
|
<p>集中查看分享状态、访问数据和文件直链。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="shares-page-actions">
|
<div class="shares-page-actions">
|
||||||
<button class="btn btn-secondary" @click="refreshShareResources">
|
<button type="button" class="btn btn-secondary share-refresh-btn" @click="refreshShareResources" aria-label="刷新分享资源">
|
||||||
<i class="fas fa-sync-alt"></i> 刷新
|
<i class="fas fa-sync-alt"></i><span class="share-action-label">刷新</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" @click="switchView('files')">
|
<button type="button" class="btn btn-primary share-create-btn" @click="switchView('files')" aria-label="创建分享" title="创建分享">
|
||||||
<i class="fas fa-plus"></i> 创建分享
|
<i class="fas fa-plus"></i><span class="share-action-label">创建分享</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="share-resource-tabs" aria-label="分享资源分类">
|
<nav class="share-resource-tabs" aria-label="分享资源分类">
|
||||||
<button :class="{ active: shareResourceTab === 'shares' }" @click="shareResourceTab = 'shares'">
|
<button type="button" :class="{ active: shareResourceTab === 'shares' }" @click="shareResourceTab = 'shares'">
|
||||||
<i class="fas fa-share-alt"></i> 分享链接
|
<i class="fas fa-share-alt"></i> 分享链接
|
||||||
<span>{{ shares.length }}</span>
|
<span>{{ shares.length }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button :class="{ active: shareResourceTab === 'directLinks' }" @click="shareResourceTab = 'directLinks'">
|
<button type="button" :class="{ active: shareResourceTab === 'directLinks' }" @click="shareResourceTab = 'directLinks'">
|
||||||
<i class="fas fa-link"></i> 文件直链
|
<i class="fas fa-link"></i> 文件直链
|
||||||
<span>{{ directLinks.length }}</span>
|
<span>{{ directLinks.length }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -3156,12 +3161,12 @@
|
|||||||
<span><i class="fas fa-shield-alt"></i> 状态 <strong>运行正常</strong></span>
|
<span><i class="fas fa-shield-alt"></i> 状态 <strong>运行正常</strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="share-toolbar shares-toolbar">
|
<div class="share-toolbar shares-toolbar" :class="{ 'share-toolbar-direct': shareResourceTab === 'directLinks' }">
|
||||||
<div class="share-search-field">
|
<div class="share-search-field">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<input type="text" v-model="shareFilters.keyword" :placeholder="shareResourceTab === 'shares' ? '搜索文件名、链接或分享码' : '搜索文件名或直链地址'">
|
<input type="text" v-model="shareFilters.keyword" :placeholder="shareResourceTab === 'shares' ? '搜索文件名、链接或分享码' : '搜索文件名或直链地址'">
|
||||||
</div>
|
</div>
|
||||||
<template v-if="shareResourceTab === 'shares'">
|
<div v-if="shareResourceTab === 'shares'" class="share-filter-controls">
|
||||||
<select v-model="shareFilters.type" aria-label="分享类型">
|
<select v-model="shareFilters.type" aria-label="分享类型">
|
||||||
<option value="all">全部类型</option>
|
<option value="all">全部类型</option>
|
||||||
<option value="file">文件</option>
|
<option value="file">文件</option>
|
||||||
@@ -3183,7 +3188,7 @@
|
|||||||
<option value="downloads_desc">下载最多</option>
|
<option value="downloads_desc">下载最多</option>
|
||||||
<option value="expire_asc">最早到期</option>
|
<option value="expire_asc">最早到期</option>
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="shareResourceTab === 'shares'" class="share-resource-panel">
|
<div v-if="shareResourceTab === 'shares'" class="share-resource-panel">
|
||||||
@@ -3295,7 +3300,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 管理员视图 -->
|
<!-- 管理员视图 -->
|
||||||
<div v-if="isLoggedIn && currentView === 'admin' && user && user.is_admin" class="main-container">
|
<div v-if="isLoggedIn && currentView === 'admin' && user && user.is_admin" class="main-container admin-main-container">
|
||||||
<!-- 管理员标签页导航 -->
|
<!-- 管理员标签页导航 -->
|
||||||
<div class="card admin-tabs-card" style="margin-bottom: 20px; padding: 0;">
|
<div class="card admin-tabs-card" style="margin-bottom: 20px; padding: 0;">
|
||||||
<div class="admin-tabs-nav">
|
<div class="admin-tabs-nav">
|
||||||
@@ -3315,9 +3320,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== 概览标签页 ========== -->
|
<!-- ========== 概览标签页 ========== -->
|
||||||
<div v-show="adminTab === 'overview'">
|
<div v-show="adminTab === 'overview'" class="admin-overview-page">
|
||||||
<!-- 调试模式开关 -->
|
<!-- 调试模式开关 -->
|
||||||
<div class="card" style="margin-bottom: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
<div class="card admin-debug-card" style="margin-bottom: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
||||||
<div class="admin-debug-row" style="display: flex; align-items: center; justify-content: space-between;">
|
<div class="admin-debug-row" style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin-bottom: 10px; color: white;">
|
<h3 style="margin-bottom: 10px; color: white;">
|
||||||
@@ -3335,7 +3340,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 服务器存储统计 -->
|
<!-- 服务器存储统计 -->
|
||||||
<div class="card" style="margin-bottom: 30px;">
|
<div class="card admin-storage-card" style="margin-bottom: 30px;">
|
||||||
<h3 style="margin-bottom: 20px;">
|
<h3 style="margin-bottom: 20px;">
|
||||||
<i class="fas fa-hdd"></i> 服务器存储统计
|
<i class="fas fa-hdd"></i> 服务器存储统计
|
||||||
</h3>
|
</h3>
|
||||||
@@ -4030,12 +4035,23 @@
|
|||||||
</div><!-- 监控标签页结束 -->
|
</div><!-- 监控标签页结束 -->
|
||||||
|
|
||||||
<!-- ========== 用户标签页 ========== -->
|
<!-- ========== 用户标签页 ========== -->
|
||||||
<div v-show="adminTab === 'users'">
|
<div v-show="adminTab === 'users'" class="admin-users-page">
|
||||||
<div class="card">
|
<div class="card admin-users-card">
|
||||||
<div class="admin-users-header">
|
<div class="admin-users-header">
|
||||||
<h3 style="margin: 0;">用户管理</h3>
|
<h3 style="margin: 0;">用户管理</h3>
|
||||||
<button class="btn btn-secondary" @click="loadUsers" :disabled="adminUsersLoading" style="min-width: 92px;">
|
<button
|
||||||
<i :class="adminUsersLoading ? 'fas fa-spinner fa-spin' : 'fas fa-rotate'"></i> 刷新
|
type="button"
|
||||||
|
class="mobile-admin-users-reset-btn"
|
||||||
|
:disabled="activeAdminUserFilterCount === 0"
|
||||||
|
@click="resetAdminUserFilters"
|
||||||
|
aria-label="重置用户筛选"
|
||||||
|
title="重置筛选"
|
||||||
|
>
|
||||||
|
<i class="fas fa-filter-circle-xmark"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary admin-users-refresh-btn" @click="loadUsers" :disabled="adminUsersLoading" style="min-width: 92px;" aria-label="刷新用户列表">
|
||||||
|
<i :class="adminUsersLoading ? 'fas fa-spinner fa-spin' : 'fas fa-rotate'"></i>
|
||||||
|
<span>刷新</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -4046,16 +4062,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin-users-filter">
|
<div class="admin-users-filter">
|
||||||
<label>角色</label>
|
<label>角色</label>
|
||||||
<select v-model="adminUserFilters.role" @change="handleAdminUsersFilterChange">
|
<select v-model="adminUserFilters.role" @change="handleAdminUsersFilterChange" aria-label="用户角色">
|
||||||
<option value="all">全部</option>
|
<option value="all">全部角色</option>
|
||||||
<option value="admin">管理员</option>
|
<option value="admin">管理员</option>
|
||||||
<option value="user">普通用户</option>
|
<option value="user">普通用户</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-users-filter">
|
<div class="admin-users-filter">
|
||||||
<label>状态</label>
|
<label>状态</label>
|
||||||
<select v-model="adminUserFilters.status" @change="handleAdminUsersFilterChange">
|
<select v-model="adminUserFilters.status" @change="handleAdminUsersFilterChange" aria-label="用户状态">
|
||||||
<option value="all">全部</option>
|
<option value="all">全部状态</option>
|
||||||
<option value="active">正常</option>
|
<option value="active">正常</option>
|
||||||
<option value="banned">已封禁</option>
|
<option value="banned">已封禁</option>
|
||||||
<option value="unverified">未激活</option>
|
<option value="unverified">未激活</option>
|
||||||
@@ -4064,8 +4080,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin-users-filter">
|
<div class="admin-users-filter">
|
||||||
<label>存储</label>
|
<label>存储</label>
|
||||||
<select v-model="adminUserFilters.storage" @change="handleAdminUsersFilterChange">
|
<select v-model="adminUserFilters.storage" @change="handleAdminUsersFilterChange" aria-label="用户存储类型">
|
||||||
<option value="all">全部</option>
|
<option value="all">全部存储</option>
|
||||||
<option value="local">当前本地</option>
|
<option value="local">当前本地</option>
|
||||||
<option value="oss">当前OSS</option>
|
<option value="oss">当前OSS</option>
|
||||||
<option value="local_only">仅本地</option>
|
<option value="local_only">仅本地</option>
|
||||||
@@ -4075,7 +4091,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin-users-filter">
|
<div class="admin-users-filter">
|
||||||
<label>排序</label>
|
<label>排序</label>
|
||||||
<select v-model="adminUserFilters.sort" @change="handleAdminUsersFilterChange">
|
<select v-model="adminUserFilters.sort" @change="handleAdminUsersFilterChange" aria-label="用户排序方式">
|
||||||
<option value="created_desc">注册时间(新到旧)</option>
|
<option value="created_desc">注册时间(新到旧)</option>
|
||||||
<option value="created_asc">注册时间(旧到新)</option>
|
<option value="created_asc">注册时间(旧到新)</option>
|
||||||
<option value="username_asc">用户名(A-Z)</option>
|
<option value="username_asc">用户名(A-Z)</option>
|
||||||
@@ -4086,10 +4102,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin-users-filter admin-users-filter-page-size">
|
<div class="admin-users-filter admin-users-filter-page-size">
|
||||||
<label>每页</label>
|
<label>每页</label>
|
||||||
<select v-model.number="adminUsersPageSize" @change="handleAdminUsersPageSizeChange">
|
<select v-model.number="adminUsersPageSize" @change="handleAdminUsersPageSizeChange" aria-label="每页用户数量">
|
||||||
<option :value="20">20</option>
|
<option :value="20">20 条</option>
|
||||||
<option :value="50">50</option>
|
<option :value="50">50 条</option>
|
||||||
<option :value="100">100</option>
|
<option :value="100">100 条</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-users-filter admin-users-filter-reset">
|
<div class="admin-users-filter admin-users-filter-reset">
|
||||||
@@ -4474,9 +4490,30 @@
|
|||||||
<i class="fas fa-trash"></i> 删除
|
<i class="fas fa-trash"></i> 删除
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 移动端新建面板 -->
|
||||||
|
<div v-if="showMobileCreateSheet" class="modal-overlay mobile-file-sheet-overlay mobile-create-sheet-overlay" @click="closeMobileCreateSheet">
|
||||||
|
<section class="mobile-file-sheet mobile-create-sheet" role="dialog" aria-modal="true" aria-labelledby="mobile-create-title" @click.stop>
|
||||||
|
<div class="mobile-file-sheet-handle"></div>
|
||||||
|
<div id="mobile-create-title" class="mobile-file-sheet-title mobile-create-sheet-title">新建</div>
|
||||||
|
<div class="mobile-create-sheet-actions">
|
||||||
|
<button type="button" class="mobile-create-option" @click="mobileCreateAction('upload')">
|
||||||
|
<span class="mobile-create-option-icon upload"><i class="fas fa-cloud-upload-alt"></i></span>
|
||||||
|
<span class="mobile-create-option-copy"><strong>上传文件</strong><small>从手机选择一个或多个文件</small></span>
|
||||||
|
<i class="fas fa-chevron-right mobile-create-option-arrow"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="mobile-create-option" @click="mobileCreateAction('folder')">
|
||||||
|
<span class="mobile-create-option-icon folder"><i class="fas fa-folder-plus"></i></span>
|
||||||
|
<span class="mobile-create-option-copy"><strong>新建文件夹</strong><small>创建目录并整理云端文件</small></span>
|
||||||
|
<i class="fas fa-chevron-right mobile-create-option-arrow"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="mobile-file-sheet-btn cancel mobile-create-cancel" @click="closeMobileCreateSheet">取消</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 移动端文件操作面板 -->
|
<!-- 移动端文件操作面板 -->
|
||||||
<div v-if="showMobileFileActionSheet && mobileActionFile" class="modal-overlay mobile-file-sheet-overlay" @click="closeMobileFileActionSheet">
|
<div v-if="showMobileFileActionSheet && mobileActionFile" class="modal-overlay mobile-file-sheet-overlay" @click="closeMobileFileActionSheet">
|
||||||
<div class="mobile-file-sheet" @click.stop>
|
<section class="mobile-file-sheet" role="dialog" aria-modal="true" aria-label="文件操作" @click.stop>
|
||||||
<div class="mobile-file-sheet-handle"></div>
|
<div class="mobile-file-sheet-handle"></div>
|
||||||
<div class="mobile-file-sheet-title" :title="getFileDisplayName(mobileActionFile)">
|
<div class="mobile-file-sheet-title" :title="getFileDisplayName(mobileActionFile)">
|
||||||
{{ getFileDisplayName(mobileActionFile) }}
|
{{ getFileDisplayName(mobileActionFile) }}
|
||||||
@@ -4508,7 +4545,7 @@
|
|||||||
<i class="fas fa-times"></i> 取消
|
<i class="fas fa-times"></i> 取消
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 管理员:编辑用户存储权限模态框 -->
|
<!-- 管理员:编辑用户存储权限模态框 -->
|
||||||
@@ -4915,7 +4952,7 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 901px) {
|
||||||
.mobile-file-sheet-overlay {
|
.mobile-file-sheet-overlay {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ createApp({
|
|||||||
return (saved && ['overview', 'settings', 'monitor', 'users'].includes(saved)) ? saved : 'overview';
|
return (saved && ['overview', 'settings', 'monitor', 'users'].includes(saved)) ? saved : 'overview';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const initialFileViewMode = (() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('fileViewMode');
|
||||||
|
if (saved && ['grid', 'list'].includes(saved)) {
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[视图] 无法读取文件视图偏好:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(max-width: 900px)').matches ? 'list' : 'grid';
|
||||||
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// API配置
|
// API配置
|
||||||
// API配置 - 通过nginx代理访问
|
// API配置 - 通过nginx代理访问
|
||||||
@@ -29,7 +42,7 @@ createApp({
|
|||||||
// 视图状态
|
// 视图状态
|
||||||
currentView: 'files',
|
currentView: 'files',
|
||||||
isLogin: true,
|
isLogin: true,
|
||||||
fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表
|
fileViewMode: initialFileViewMode, // 移动端默认列表视图,并记住用户选择
|
||||||
shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表
|
shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表
|
||||||
shareResourceTab: 'shares', // 分享中心资源标签: shares / directLinks
|
shareResourceTab: 'shares', // 分享中心资源标签: shares / directLinks
|
||||||
settingsTab: 'storage', // 设置中心标签: storage / appearance / security
|
settingsTab: 'storage', // 设置中心标签: storage / appearance / security
|
||||||
@@ -141,7 +154,6 @@ createApp({
|
|||||||
status: 'all', // all/active/expiring/expired/protected/public
|
status: 'all', // all/active/expiring/expired/protected/public
|
||||||
sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc
|
sort: 'created_desc' // created_desc/created_asc/views_desc/downloads_desc/expire_asc
|
||||||
},
|
},
|
||||||
|
|
||||||
// 文件重命名
|
// 文件重命名
|
||||||
showRenameModal: false,
|
showRenameModal: false,
|
||||||
renameForm: {
|
renameForm: {
|
||||||
@@ -349,6 +361,7 @@ createApp({
|
|||||||
contextMenuX: 0,
|
contextMenuX: 0,
|
||||||
contextMenuY: 0,
|
contextMenuY: 0,
|
||||||
contextMenuFile: null,
|
contextMenuFile: null,
|
||||||
|
showMobileCreateSheet: false,
|
||||||
showMobileFileActionSheet: false,
|
showMobileFileActionSheet: false,
|
||||||
mobileActionFile: null,
|
mobileActionFile: null,
|
||||||
|
|
||||||
@@ -715,6 +728,17 @@ createApp({
|
|||||||
return list;
|
return list;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
activeAdminUserFilterCount() {
|
||||||
|
let count = 0;
|
||||||
|
if (String(this.adminUserFilters.keyword || '').trim()) count += 1;
|
||||||
|
if (this.adminUserFilters.role !== 'all') count += 1;
|
||||||
|
if (this.adminUserFilters.status !== 'all') count += 1;
|
||||||
|
if (this.adminUserFilters.storage !== 'all') count += 1;
|
||||||
|
if (this.adminUserFilters.sort !== 'created_desc') count += 1;
|
||||||
|
if (Number(this.adminUsersPageSize) !== 20) count += 1;
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
|
||||||
adminUsersFilteredCount() {
|
adminUsersFilteredCount() {
|
||||||
return Math.max(0, Number(this.adminUsersTotalCount) || 0);
|
return Math.max(0, Number(this.adminUsersTotalCount) || 0);
|
||||||
},
|
},
|
||||||
@@ -2237,7 +2261,7 @@ handleDragLeave(e) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isMobileViewport() {
|
isMobileViewport() {
|
||||||
return window.matchMedia('(max-width: 768px)').matches;
|
return window.matchMedia('(max-width: 900px)').matches;
|
||||||
},
|
},
|
||||||
|
|
||||||
setMobileSheetScrollLock(locked) {
|
setMobileSheetScrollLock(locked) {
|
||||||
@@ -2245,6 +2269,46 @@ handleDragLeave(e) {
|
|||||||
document.body.style.overflow = locked ? 'hidden' : '';
|
document.body.style.overflow = locked ? 'hidden' : '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFileViewMode(mode) {
|
||||||
|
if (!['grid', 'list'].includes(mode)) return;
|
||||||
|
this.fileViewMode = mode;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('fileViewMode', mode);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[视图] 无法保存文件视图偏好:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openMobileCreateSheet(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeMobileFileActionSheet();
|
||||||
|
this.showMobileCreateSheet = true;
|
||||||
|
this.setMobileSheetScrollLock(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMobileCreateSheet() {
|
||||||
|
this.showMobileCreateSheet = false;
|
||||||
|
this.setMobileSheetScrollLock(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
mobileCreateAction(action) {
|
||||||
|
this.closeMobileCreateSheet();
|
||||||
|
|
||||||
|
if (action === 'upload') {
|
||||||
|
this.$nextTick(() => this.$refs.fileUploadInput?.click());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'folder') {
|
||||||
|
this.showCreateFolderModal = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
openMobileFileActionSheet(file, event) {
|
openMobileFileActionSheet(file, event) {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -5176,11 +5240,12 @@ handleDragLeave(e) {
|
|||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加 ESC 键监听(按 ESC 关闭拖拽覆盖层)
|
// 添加 ESC 键监听(按 ESC 关闭当前浮层)
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape" && this.isDragging) {
|
if (e.key !== "Escape") return;
|
||||||
this.isDragging = false;
|
if (this.isDragging) this.isDragging = false;
|
||||||
}
|
if (this.showMobileCreateSheet) this.closeMobileCreateSheet();
|
||||||
|
if (this.showMobileFileActionSheet) this.closeMobileFileActionSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置axios响应拦截器,处理401错误(token过期/失效)
|
// 设置axios响应拦截器,处理401错误(token过期/失效)
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
import { createHash } from 'node:crypto';
|
||||||
|
import {
|
||||||
|
copyFileSync,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
|
writeFileSync
|
||||||
|
} from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@@ -32,3 +40,26 @@ for (const entry of ['libs', 'favicon.ico']) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vite hoists linked stylesheets into <head>, ahead of legacy inline styles in
|
||||||
|
// app.html. Re-emit the workspace layer after those styles so production and
|
||||||
|
// development use the same cascade order.
|
||||||
|
const workspaceSourcePath = path.join(frontendRoot, 'workspace.css');
|
||||||
|
const workspaceContents = readFileSync(workspaceSourcePath);
|
||||||
|
const workspaceHash = createHash('sha256').update(workspaceContents).digest('hex').slice(0, 12);
|
||||||
|
const workspaceFileName = `workspace-${workspaceHash}.css`;
|
||||||
|
copyFileSync(workspaceSourcePath, path.join(outputDirectory, workspaceFileName));
|
||||||
|
|
||||||
|
const builtAppPath = path.join(outputDirectory, 'app.html');
|
||||||
|
const builtAppHtml = readFileSync(builtAppPath, 'utf8');
|
||||||
|
const runtimeStylesheet = ` <link rel="stylesheet" href="/${workspaceFileName}" data-workspace-runtime>\n`;
|
||||||
|
const nextBuiltAppHtml = builtAppHtml.includes('data-workspace-runtime')
|
||||||
|
? builtAppHtml.replace(/\s*<link[^>]+data-workspace-runtime[^>]*>\s*/u, `\n${runtimeStylesheet}`)
|
||||||
|
: builtAppHtml.replace('</body>', `${runtimeStylesheet}</body>`);
|
||||||
|
|
||||||
|
if (!nextBuiltAppHtml.includes('data-workspace-runtime')) {
|
||||||
|
throw new Error('Unable to inject the runtime workspace stylesheet into dist/app.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(builtAppPath, nextBuiltAppHtml, 'utf8');
|
||||||
|
console.log(`Injected runtime workspace stylesheet: ${workspaceFileName}`);
|
||||||
|
|||||||
@@ -32,4 +32,21 @@ for (const fileName of ['app.html', 'share.html']) {
|
|||||||
assert.strictEqual(/vue\.global(?:\.prod)?\.js|axios\.min\.js/.test(html), false, `${fileName} still loads global Vue/Axios`);
|
assert.strictEqual(/vue\.global(?:\.prod)?\.js|axios\.min\.js/.test(html), false, `${fileName} still loads global Vue/Axios`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const builtAppHtml = readFileSync(path.join(outputDirectory, 'app.html'), 'utf8');
|
||||||
|
const runtimeWorkspaceMatch = builtAppHtml.match(
|
||||||
|
/<link rel="stylesheet" href="\/(workspace-[a-f0-9]{12}\.css)" data-workspace-runtime>/
|
||||||
|
);
|
||||||
|
assert.ok(runtimeWorkspaceMatch, 'app.html is missing the hashed runtime workspace stylesheet');
|
||||||
|
|
||||||
|
const runtimeWorkspacePath = path.join(outputDirectory, runtimeWorkspaceMatch[1]);
|
||||||
|
assert.ok(existsSync(runtimeWorkspacePath), 'runtime workspace stylesheet is missing');
|
||||||
|
assert.ok(
|
||||||
|
builtAppHtml.indexOf(runtimeWorkspaceMatch[0]) > builtAppHtml.lastIndexOf('</style>'),
|
||||||
|
'runtime workspace stylesheet must load after every legacy inline style'
|
||||||
|
);
|
||||||
|
|
||||||
|
const runtimeWorkspaceCss = readFileSync(runtimeWorkspacePath, 'utf8');
|
||||||
|
assert.ok(runtimeWorkspaceCss.includes('.mobile-file-create-trigger'), 'runtime workspace stylesheet is incomplete');
|
||||||
|
assert.ok(runtimeWorkspaceCss.includes('@media (max-width: 900px)'), 'runtime workspace mobile breakpoint is missing');
|
||||||
|
|
||||||
console.log(`Verified ${htmlFiles.length} HTML entries and their emitted assets.`);
|
console.log(`Verified ${htmlFiles.length} HTML entries and their emitted assets.`);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
530
install.sh
530
install.sh
@@ -27,7 +27,7 @@ YELLOW='\033[1;33m'
|
|||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
PURPLE='\033[0;35m'
|
PURPLE='\033[0;35m'
|
||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
WHITE='\033[1;37m'
|
GRAY='\033[0;37m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# 全局变量
|
# 全局变量
|
||||||
@@ -50,7 +50,7 @@ BACKEND_PORT="40001"
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
print_banner() {
|
print_banner() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${CYAN}"
|
echo -e "${CYAN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -82,6 +82,129 @@ print_info() {
|
|||||||
echo -e "${CYAN}ℹ $1${NC}"
|
echo -e "${CYAN}ℹ $1${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 非交互环境没有 TERM 时,clear 会在 set -e 下直接终止安装。
|
||||||
|
clear_screen() {
|
||||||
|
if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && command -v clear &> /dev/null; then
|
||||||
|
clear 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
has_controlling_terminal() {
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e /dev/tty ]] && (exec 3</dev/tty) 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_network_connectivity() {
|
||||||
|
if command -v timeout &> /dev/null; then
|
||||||
|
timeout 10 bash -c 'exec 3<>/dev/tcp/git.workyai.cn/443' 2>/dev/null
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
curl -fsS --connect-timeout 5 --max-time 10 -o /dev/null https://git.workyai.cn/
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v wget &> /dev/null; then
|
||||||
|
wget -q --spider --timeout=10 https://git.workyai.cn/
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
is_ip_address() {
|
||||||
|
local value="$1"
|
||||||
|
local part
|
||||||
|
local -a ipv4_parts
|
||||||
|
|
||||||
|
if [[ "$value" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||||
|
IFS='.' read -r -a ipv4_parts <<< "$value"
|
||||||
|
for part in "${ipv4_parts[@]}"; do
|
||||||
|
if ((10#$part > 255)); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$value" == *:* ]] && [[ "$value" =~ ^[0-9a-fA-F:]+$ ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_public_ip() {
|
||||||
|
local endpoint
|
||||||
|
local ip=""
|
||||||
|
|
||||||
|
for endpoint in "https://api.ipify.org" "https://ifconfig.me/ip" "https://icanhazip.com"; do
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
ip=$(curl -4fsS --connect-timeout 3 --max-time 5 "$endpoint" 2>/dev/null || true)
|
||||||
|
elif command -v wget &> /dev/null; then
|
||||||
|
ip=$(wget -qO- --timeout=5 "$endpoint" 2>/dev/null || true)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
ip=${ip//$'\r'/}
|
||||||
|
ip=${ip//$'\n'/}
|
||||||
|
if is_ip_address "$ip"; then
|
||||||
|
printf '%s' "$ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
format_url_host() {
|
||||||
|
local host="$1"
|
||||||
|
if [[ "$host" == *:* ]] && [[ "$host" != \[*\] ]]; then
|
||||||
|
printf '[%s]' "$host"
|
||||||
|
else
|
||||||
|
printf '%s' "$host"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dotenv_quote() {
|
||||||
|
local value="$1"
|
||||||
|
value=${value//\\/\\\\}
|
||||||
|
value=${value//\"/\\\"}
|
||||||
|
value=${value//$'\r'/\\r}
|
||||||
|
value=${value//$'\n'/\\n}
|
||||||
|
printf '"%s"' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_and_run_setup_script() {
|
||||||
|
local url="$1"
|
||||||
|
shift
|
||||||
|
local setup_script
|
||||||
|
setup_script=$(mktemp)
|
||||||
|
|
||||||
|
if ! curl -fsSL --connect-timeout 10 --max-time 120 "$url" -o "$setup_script"; then
|
||||||
|
rm -f "$setup_script"
|
||||||
|
print_error "下载远程安装脚本失败: $url"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! bash "$setup_script" "$@"; then
|
||||||
|
rm -f "$setup_script"
|
||||||
|
print_error "执行远程安装脚本失败: $url"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$setup_script"
|
||||||
|
}
|
||||||
|
|
||||||
# 检测操作系统
|
# 检测操作系统
|
||||||
detect_os() {
|
detect_os() {
|
||||||
if [[ -f /etc/os-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
@@ -190,18 +313,25 @@ system_check() {
|
|||||||
|
|
||||||
# 检测操作系统
|
# 检测操作系统
|
||||||
detect_os
|
detect_os
|
||||||
print_success "操作系统: $OS $OS_VERSION"
|
print_success "操作系统: ${OS_NAME:-$OS} $OS_VERSION"
|
||||||
|
|
||||||
# 检测架构
|
# 检测架构
|
||||||
detect_arch
|
detect_arch
|
||||||
print_success "系统架构: $ARCH"
|
print_success "系统架构: $ARCH"
|
||||||
|
|
||||||
# 检测内存
|
# 检测内存;最小化系统可能尚未安装 procps/free。
|
||||||
|
if command -v free &> /dev/null; then
|
||||||
TOTAL_MEM=$(free -m | awk '/^Mem:/{print $2}')
|
TOTAL_MEM=$(free -m | awk '/^Mem:/{print $2}')
|
||||||
if [[ $TOTAL_MEM -lt 512 ]]; then
|
else
|
||||||
|
TOTAL_MEM=$(awk '/^MemTotal:/{printf "%d", $2 / 1024}' /proc/meminfo 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
TOTAL_MEM=${TOTAL_MEM:-0}
|
||||||
|
if [[ $TOTAL_MEM -eq 0 ]]; then
|
||||||
|
print_warning "无法检测系统内存"
|
||||||
|
elif [[ $TOTAL_MEM -lt 512 ]]; then
|
||||||
print_warning "内存不足512MB,可能影响性能"
|
print_warning "内存不足512MB,可能影响性能"
|
||||||
else
|
else
|
||||||
print_success "可用内存: ${TOTAL_MEM}MB"
|
print_success "系统内存: ${TOTAL_MEM}MB"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检测磁盘空间
|
# 检测磁盘空间
|
||||||
@@ -212,17 +342,22 @@ system_check() {
|
|||||||
print_success "可用磁盘: ${DISK_AVAIL}MB"
|
print_success "可用磁盘: ${DISK_AVAIL}MB"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检测网络
|
# 直接检查实际需要访问的 Git HTTPS 服务,不依赖最小系统通常缺失的 ping。
|
||||||
if ping -c 1 git.workyai.cn &> /dev/null; then
|
if check_network_connectivity; then
|
||||||
print_success "网络连接正常"
|
print_success "网络连接正常"
|
||||||
else
|
else
|
||||||
print_error "无法连接到网络"
|
network_status=$?
|
||||||
|
if [[ $network_status -eq 2 ]]; then
|
||||||
|
print_warning "缺少网络探测工具,将由软件包安装与 Git 下载继续验证网络"
|
||||||
|
else
|
||||||
|
print_error "无法连接到 git.workyai.cn:443"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 检测公网IP
|
# 检测公网IP
|
||||||
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "未知")
|
PUBLIC_IP=$(get_public_ip || true)
|
||||||
print_info "公网IP: $PUBLIC_IP"
|
print_info "公网IP: ${PUBLIC_IP:-未知}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
@@ -266,33 +401,67 @@ configure_aliyun_mirror() {
|
|||||||
|
|
||||||
case $OS in
|
case $OS in
|
||||||
ubuntu)
|
ubuntu)
|
||||||
# 备份原有源
|
local ubuntu_codename="${VERSION_CODENAME:-}"
|
||||||
if [[ ! -f /etc/apt/sources.list.bak ]]; then
|
if [[ -z "$ubuntu_codename" ]] && command -v lsb_release &> /dev/null; then
|
||||||
cp /etc/apt/sources.list /etc/apt/sources.list.bak
|
ubuntu_codename=$(lsb_release -cs)
|
||||||
|
fi
|
||||||
|
if [[ -z "$ubuntu_codename" ]]; then
|
||||||
|
print_error "无法确定 Ubuntu 版本代号,未修改软件源"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 配置Ubuntu阿里云源
|
# Ubuntu 24.04+ 默认使用 deb822 格式,旧版本仍使用 sources.list。
|
||||||
|
if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then
|
||||||
|
if [[ ! -f /etc/apt/sources.list.d/ubuntu.sources.bak ]]; then
|
||||||
|
cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak
|
||||||
|
fi
|
||||||
|
sed -Ei \
|
||||||
|
-e 's|https?://([^/]+\.)?archive\.ubuntu\.com/ubuntu/?|https://mirrors.aliyun.com/ubuntu/|g' \
|
||||||
|
-e 's|https?://security\.ubuntu\.com/ubuntu/?|https://mirrors.aliyun.com/ubuntu/|g' \
|
||||||
|
-e 's|https?://ports\.ubuntu\.com/ubuntu-ports/?|https://mirrors.aliyun.com/ubuntu-ports/|g' \
|
||||||
|
/etc/apt/sources.list.d/ubuntu.sources
|
||||||
|
else
|
||||||
|
if [[ -f /etc/apt/sources.list ]] && [[ ! -f /etc/apt/sources.list.bak ]]; then
|
||||||
|
cp /etc/apt/sources.list /etc/apt/sources.list.bak
|
||||||
|
fi
|
||||||
cat > /etc/apt/sources.list << EOF
|
cat > /etc/apt/sources.list << EOF
|
||||||
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs) main restricted universe multiverse
|
deb https://mirrors.aliyun.com/ubuntu/ ${ubuntu_codename} main restricted universe multiverse
|
||||||
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-updates main restricted universe multiverse
|
deb https://mirrors.aliyun.com/ubuntu/ ${ubuntu_codename}-updates main restricted universe multiverse
|
||||||
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-backports main restricted universe multiverse
|
deb https://mirrors.aliyun.com/ubuntu/ ${ubuntu_codename}-backports main restricted universe multiverse
|
||||||
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-security main restricted universe multiverse
|
deb https://mirrors.aliyun.com/ubuntu/ ${ubuntu_codename}-security main restricted universe multiverse
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
print_success "阿里云源配置完成"
|
print_success "阿里云源配置完成"
|
||||||
;;
|
;;
|
||||||
debian)
|
debian)
|
||||||
# 备份原有源
|
local debian_codename="${VERSION_CODENAME:-}"
|
||||||
if [[ ! -f /etc/apt/sources.list.bak ]]; then
|
if [[ -z "$debian_codename" ]] && command -v lsb_release &> /dev/null; then
|
||||||
cp /etc/apt/sources.list /etc/apt/sources.list.bak
|
debian_codename=$(lsb_release -cs)
|
||||||
|
fi
|
||||||
|
if [[ -z "$debian_codename" ]]; then
|
||||||
|
print_error "无法确定 Debian 版本代号,未修改软件源"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 配置Debian阿里云源
|
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
|
||||||
|
if [[ ! -f /etc/apt/sources.list.d/debian.sources.bak ]]; then
|
||||||
|
cp /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list.d/debian.sources.bak
|
||||||
|
fi
|
||||||
|
sed -Ei \
|
||||||
|
-e 's|https?://deb\.debian\.org/debian/?|https://mirrors.aliyun.com/debian/|g' \
|
||||||
|
-e 's|https?://(security\.debian\.org|deb\.debian\.org)/debian-security/?|https://mirrors.aliyun.com/debian-security/|g' \
|
||||||
|
/etc/apt/sources.list.d/debian.sources
|
||||||
|
else
|
||||||
|
if [[ -f /etc/apt/sources.list ]] && [[ ! -f /etc/apt/sources.list.bak ]]; then
|
||||||
|
cp /etc/apt/sources.list /etc/apt/sources.list.bak
|
||||||
|
fi
|
||||||
cat > /etc/apt/sources.list << EOF
|
cat > /etc/apt/sources.list << EOF
|
||||||
deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs) main contrib non-free non-free-firmware
|
deb https://mirrors.aliyun.com/debian/ ${debian_codename} main contrib non-free non-free-firmware
|
||||||
deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs)-updates main contrib non-free non-free-firmware
|
deb https://mirrors.aliyun.com/debian/ ${debian_codename}-updates main contrib non-free non-free-firmware
|
||||||
deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs)-backports main contrib non-free non-free-firmware
|
deb https://mirrors.aliyun.com/debian/ ${debian_codename}-backports main contrib non-free non-free-firmware
|
||||||
deb http://mirrors.aliyun.com/debian-security $(lsb_release -cs)-security main contrib non-free non-free-firmware
|
deb https://mirrors.aliyun.com/debian-security/ ${debian_codename}-security main contrib non-free non-free-firmware
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
print_success "阿里云源配置完成"
|
print_success "阿里云源配置完成"
|
||||||
;;
|
;;
|
||||||
centos)
|
centos)
|
||||||
@@ -497,22 +666,31 @@ install_dependencies() {
|
|||||||
case $PKG_MANAGER in
|
case $PKG_MANAGER in
|
||||||
apt)
|
apt)
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y curl wget git unzip lsb-release build-essential python3
|
apt-get install -y \
|
||||||
|
ca-certificates curl wget git unzip lsb-release gnupg openssl \
|
||||||
|
build-essential software-properties-common python3 \
|
||||||
|
iproute2 iputils-ping dnsutils procps cron
|
||||||
install_nodejs_apt
|
install_nodejs_apt
|
||||||
install_nginx_apt
|
install_nginx_apt
|
||||||
;;
|
;;
|
||||||
yum)
|
yum)
|
||||||
yum install -y curl wget git unzip redhat-lsb-core gcc-c++ make python3
|
yum install -y \
|
||||||
|
ca-certificates curl wget git unzip redhat-lsb-core openssl \
|
||||||
|
gcc-c++ make python3 iproute bind-utils procps-ng cronie
|
||||||
install_nodejs_yum
|
install_nodejs_yum
|
||||||
install_nginx_yum
|
install_nginx_yum
|
||||||
;;
|
;;
|
||||||
dnf)
|
dnf)
|
||||||
dnf install -y curl wget git unzip redhat-lsb-core gcc-c++ make python3
|
dnf install -y \
|
||||||
|
ca-certificates curl wget git unzip redhat-lsb-core openssl \
|
||||||
|
gcc-c++ make python3 iproute bind-utils procps-ng cronie
|
||||||
install_nodejs_dnf
|
install_nodejs_dnf
|
||||||
install_nginx_dnf
|
install_nginx_dnf
|
||||||
;;
|
;;
|
||||||
zypper)
|
zypper)
|
||||||
zypper install -y curl wget git unzip lsb-release gcc-c++ make python3
|
zypper install -y \
|
||||||
|
ca-certificates curl wget git unzip lsb-release openssl \
|
||||||
|
gcc-c++ make python3 iproute2 bind-utils procps cron
|
||||||
install_nodejs_zypper
|
install_nodejs_zypper
|
||||||
install_nginx_zypper
|
install_nginx_zypper
|
||||||
;;
|
;;
|
||||||
@@ -534,6 +712,14 @@ node_version_supported() {
|
|||||||
node -e "const [major, minor] = process.versions.node.split('.').map(Number); process.exit((major > 20 && major < 25) || (major === 20 && minor >= 19) ? 0 : 1)" 2>/dev/null
|
node -e "const [major, minor] = process.versions.node.split('.').map(Number); process.exit((major > 20 && major < 25) || (major === 20 && minor >= 19) ? 0 : 1)" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify_nodejs_installation() {
|
||||||
|
if ! command -v node &> /dev/null || ! node_version_supported; then
|
||||||
|
print_error "Node.js 安装结果不满足版本要求(需要 ${NODE_MIN_VERSION} 至 24.x)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
print_success "Node.js 安装完成: $(node -v)"
|
||||||
|
}
|
||||||
|
|
||||||
install_nodejs_apt() {
|
install_nodejs_apt() {
|
||||||
if command -v node &> /dev/null; then
|
if command -v node &> /dev/null; then
|
||||||
if node_version_supported; then
|
if node_version_supported; then
|
||||||
@@ -544,9 +730,9 @@ install_nodejs_apt() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
||||||
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
download_and_run_setup_script "https://deb.nodesource.com/setup_${NODE_VERSION}.x"
|
||||||
apt-get install -y nodejs
|
apt-get install -y nodejs
|
||||||
print_success "Node.js 安装完成: $(node -v)"
|
verify_nodejs_installation
|
||||||
}
|
}
|
||||||
|
|
||||||
install_nodejs_yum() {
|
install_nodejs_yum() {
|
||||||
@@ -559,9 +745,9 @@ install_nodejs_yum() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
||||||
curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
download_and_run_setup_script "https://rpm.nodesource.com/setup_${NODE_VERSION}.x"
|
||||||
yum install -y nodejs
|
yum install -y nodejs
|
||||||
print_success "Node.js 安装完成: $(node -v)"
|
verify_nodejs_installation
|
||||||
}
|
}
|
||||||
|
|
||||||
install_nginx_apt() {
|
install_nginx_apt() {
|
||||||
@@ -598,9 +784,9 @@ install_nodejs_dnf() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
||||||
curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
download_and_run_setup_script "https://rpm.nodesource.com/setup_${NODE_VERSION}.x"
|
||||||
dnf install -y nodejs
|
dnf install -y nodejs
|
||||||
print_success "Node.js 安装完成: $(node -v)"
|
verify_nodejs_installation
|
||||||
}
|
}
|
||||||
|
|
||||||
install_nginx_dnf() {
|
install_nginx_dnf() {
|
||||||
@@ -627,7 +813,7 @@ install_nodejs_zypper() {
|
|||||||
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
|
||||||
# openSUSE使用官方仓库的Node.js
|
# openSUSE使用官方仓库的Node.js
|
||||||
zypper install -y nodejs${NODE_VERSION}
|
zypper install -y nodejs${NODE_VERSION}
|
||||||
print_success "Node.js 安装完成: $(node -v)"
|
verify_nodejs_installation
|
||||||
}
|
}
|
||||||
|
|
||||||
install_nginx_zypper() {
|
install_nginx_zypper() {
|
||||||
@@ -645,12 +831,16 @@ install_nginx_zypper() {
|
|||||||
install_pm2() {
|
install_pm2() {
|
||||||
if command -v pm2 &> /dev/null; then
|
if command -v pm2 &> /dev/null; then
|
||||||
print_success "PM2 已安装: $(pm2 -v)"
|
print_success "PM2 已安装: $(pm2 -v)"
|
||||||
return
|
else
|
||||||
fi
|
|
||||||
|
|
||||||
print_info "正在安装 PM2..."
|
print_info "正在安装 PM2..."
|
||||||
npm install -g pm2
|
npm install -g pm2
|
||||||
pm2 startup
|
fi
|
||||||
|
|
||||||
|
if command -v systemctl &> /dev/null && [[ -d /run/systemd/system ]]; then
|
||||||
|
pm2 startup systemd -u root --hp /root
|
||||||
|
else
|
||||||
|
print_warning "未检测到 systemd,无法自动配置 PM2 开机启动"
|
||||||
|
fi
|
||||||
print_success "PM2 安装完成"
|
print_success "PM2 安装完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,8 +1231,18 @@ choose_access_mode() {
|
|||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
USE_DOMAIN=false
|
USE_DOMAIN=false
|
||||||
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "未知")
|
PUBLIC_IP=$(get_public_ip || true)
|
||||||
print_info "将使用 IP 模式访问: http://${PUBLIC_IP}"
|
if [[ -z "$PUBLIC_IP" ]]; then
|
||||||
|
while true; do
|
||||||
|
read -p "无法自动获取公网 IP,请手动输入服务器 IP: " PUBLIC_IP < /dev/tty
|
||||||
|
if is_ip_address "$PUBLIC_IP"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
print_error "IP 地址格式不正确"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
PUBLIC_URL_HOST=$(format_url_host "$PUBLIC_IP")
|
||||||
|
print_info "将使用 IP 模式访问: http://${PUBLIC_URL_HOST}"
|
||||||
echo ""
|
echo ""
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
@@ -1071,7 +1271,7 @@ configure_domain() {
|
|||||||
# 验证域名解析
|
# 验证域名解析
|
||||||
print_info "正在验证域名解析..."
|
print_info "正在验证域名解析..."
|
||||||
DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n1)
|
DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n1)
|
||||||
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com)
|
PUBLIC_IP=$(get_public_ip || true)
|
||||||
|
|
||||||
if [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then
|
if [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then
|
||||||
print_success "域名已正确解析到当前服务器IP"
|
print_success "域名已正确解析到当前服务器IP"
|
||||||
@@ -1152,53 +1352,47 @@ choose_ssl_method() {
|
|||||||
print_step "选择SSL证书部署方式"
|
print_step "选择SSL证书部署方式"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}【推荐方案】${NC}"
|
echo -e "${YELLOW}【推荐方案】${NC}"
|
||||||
echo -e "${GREEN}[1]${NC} acme.sh + Let's Encrypt"
|
echo -e "${GREEN}[1]${NC} Certbot + Let's Encrypt"
|
||||||
echo " - 纯Shell脚本,轻量级稳定"
|
echo " - 官方客户端,自动续期"
|
||||||
echo " - 自动续期,无需手动操作"
|
echo " - 自动续期,无需手动操作"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}【备选方案】${NC}"
|
echo -e "${YELLOW}【备选方案】${NC}"
|
||||||
echo -e "${GREEN}[2]${NC} acme.sh + ZeroSSL"
|
echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
|
||||||
|
echo " - 纯Shell脚本,轻量级稳定"
|
||||||
|
echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
|
||||||
echo " - Let's Encrypt 的免费替代品"
|
echo " - Let's Encrypt 的免费替代品"
|
||||||
echo -e "${GREEN}[3]${NC} acme.sh + Buypass"
|
echo -e "${GREEN}[4]${NC} acme.sh + Buypass"
|
||||||
echo " - 挪威免费CA,有效期180天"
|
echo " - 挪威免费CA,有效期180天"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}【云服务商证书】${NC}"
|
|
||||||
echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)"
|
|
||||||
echo -e "${GREEN}[5]${NC} 腾讯云免费证书 (需提供SecretKey)"
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}【其他选项】${NC}"
|
echo -e "${YELLOW}【其他选项】${NC}"
|
||||||
echo -e "${GREEN}[6]${NC} 使用已有证书 (手动上传)"
|
echo -e "${GREEN}[5]${NC} 使用已有证书 (手动上传)"
|
||||||
echo -e "${GREEN}[7]${NC} 暂不配置HTTPS (可后续配置)"
|
echo -e "${GREEN}[6]${NC} 暂不配置HTTPS (可后续配置)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -p "请输入选项 [1-7]: " ssl_choice < /dev/tty
|
read -p "请输入选项 [1-6]: " ssl_choice < /dev/tty
|
||||||
case $ssl_choice in
|
case $ssl_choice in
|
||||||
1)
|
1)
|
||||||
SSL_METHOD="2" # acme.sh + Let's Encrypt
|
SSL_METHOD="1" # Certbot + Let's Encrypt
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
SSL_METHOD="3" # acme.sh + ZeroSSL
|
SSL_METHOD="2" # acme.sh + Let's Encrypt
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
SSL_METHOD="5" # acme.sh + Buypass
|
SSL_METHOD="3" # acme.sh + ZeroSSL
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
SSL_METHOD="4" # 阿里云
|
SSL_METHOD="5" # acme.sh + Buypass
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
SSL_METHOD="6" # 腾讯云
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
6)
|
|
||||||
SSL_METHOD="7" # 手动上传
|
SSL_METHOD="7" # 手动上传
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
7)
|
6)
|
||||||
SSL_METHOD="8" # 不配置HTTPS
|
SSL_METHOD="8" # 不配置HTTPS
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
@@ -1216,21 +1410,18 @@ deploy_ssl() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
case $SSL_METHOD in
|
case $SSL_METHOD in
|
||||||
|
1)
|
||||||
|
deploy_certbot || ssl_fallback "1"
|
||||||
|
;;
|
||||||
2)
|
2)
|
||||||
deploy_acme_letsencrypt || ssl_fallback "2"
|
deploy_acme_letsencrypt || ssl_fallback "2"
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
deploy_acme_zerossl || ssl_fallback "3"
|
deploy_acme_zerossl || ssl_fallback "3"
|
||||||
;;
|
;;
|
||||||
4)
|
|
||||||
deploy_aliyun_ssl || ssl_fallback "4"
|
|
||||||
;;
|
|
||||||
5)
|
5)
|
||||||
deploy_acme_buypass || ssl_fallback "5"
|
deploy_acme_buypass || ssl_fallback "5"
|
||||||
;;
|
;;
|
||||||
6)
|
|
||||||
deploy_tencent_ssl || ssl_fallback "6"
|
|
||||||
;;
|
|
||||||
7)
|
7)
|
||||||
deploy_manual_ssl
|
deploy_manual_ssl
|
||||||
;;
|
;;
|
||||||
@@ -1252,6 +1443,12 @@ ssl_fallback() {
|
|||||||
# 动态显示可用选项(排除已失败的)
|
# 动态显示可用选项(排除已失败的)
|
||||||
local available_options=()
|
local available_options=()
|
||||||
|
|
||||||
|
# 方案1: Certbot + Let's Encrypt
|
||||||
|
if [[ "$failed_method" != "1" ]]; then
|
||||||
|
echo -e "${GREEN}[1]${NC} Certbot + Let's Encrypt"
|
||||||
|
available_options+=("1")
|
||||||
|
fi
|
||||||
|
|
||||||
# 方案2: acme.sh + Let's Encrypt
|
# 方案2: acme.sh + Let's Encrypt
|
||||||
if [[ "$failed_method" != "2" ]]; then
|
if [[ "$failed_method" != "2" ]]; then
|
||||||
echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
|
echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
|
||||||
@@ -1282,12 +1479,17 @@ ssl_fallback() {
|
|||||||
read -p "请选择备选方案: " retry_choice < /dev/tty
|
read -p "请选择备选方案: " retry_choice < /dev/tty
|
||||||
|
|
||||||
# 检查输入是否在可用选项中
|
# 检查输入是否在可用选项中
|
||||||
if [[ ! " ${available_options[@]} " =~ " ${retry_choice} " ]]; then
|
if [[ " ${available_options[*]} " != *" ${retry_choice} "* ]]; then
|
||||||
print_error "无效选项或该方案已失败"
|
print_error "无效选项或该方案已失败"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case $retry_choice in
|
case $retry_choice in
|
||||||
|
1)
|
||||||
|
deploy_certbot && return 0
|
||||||
|
ssl_fallback "1"
|
||||||
|
return $?
|
||||||
|
;;
|
||||||
2)
|
2)
|
||||||
deploy_acme_letsencrypt && return 0
|
deploy_acme_letsencrypt && return 0
|
||||||
# 如果再次失败,继续调用fallback但排除方案2
|
# 如果再次失败,继续调用fallback但排除方案2
|
||||||
@@ -1334,25 +1536,17 @@ deploy_certbot() {
|
|||||||
print_success "Certbot (snap版) 安装成功"
|
print_success "Certbot (snap版) 安装成功"
|
||||||
else
|
else
|
||||||
print_warning "snap安装失败,尝试apt安装..."
|
print_warning "snap安装失败,尝试apt安装..."
|
||||||
# 修复urllib3依赖问题
|
|
||||||
apt-get remove -y python3-urllib3 2>/dev/null || true
|
|
||||||
apt-get install -y certbot python3-certbot-nginx
|
apt-get install -y certbot python3-certbot-nginx
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
print_info "snap不可用,使用apt安装..."
|
print_info "snap不可用,使用apt安装..."
|
||||||
# 修复urllib3依赖问题
|
|
||||||
apt-get remove -y python3-urllib3 2>/dev/null || true
|
|
||||||
apt-get install -y certbot python3-certbot-nginx
|
apt-get install -y certbot python3-certbot-nginx
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
yum)
|
yum)
|
||||||
# 修复urllib3依赖问题
|
|
||||||
yum remove -y python3-urllib3 2>/dev/null || true
|
|
||||||
yum install -y certbot python3-certbot-nginx
|
yum install -y certbot python3-certbot-nginx
|
||||||
;;
|
;;
|
||||||
dnf)
|
dnf)
|
||||||
# 修复urllib3依赖问题
|
|
||||||
dnf remove -y python3-urllib3 2>/dev/null || true
|
|
||||||
dnf install -y certbot python3-certbot-nginx
|
dnf install -y certbot python3-certbot-nginx
|
||||||
;;
|
;;
|
||||||
zypper)
|
zypper)
|
||||||
@@ -1374,11 +1568,9 @@ deploy_certbot() {
|
|||||||
print_warning "检测到Certbot依赖问题,正在修复..."
|
print_warning "检测到Certbot依赖问题,正在修复..."
|
||||||
case $PKG_MANAGER in
|
case $PKG_MANAGER in
|
||||||
apt)
|
apt)
|
||||||
apt-get remove -y python3-urllib3 2>/dev/null || true
|
|
||||||
apt-get install --reinstall -y certbot python3-certbot-nginx
|
apt-get install --reinstall -y certbot python3-certbot-nginx
|
||||||
;;
|
;;
|
||||||
yum|dnf)
|
yum|dnf)
|
||||||
$PKG_MANAGER remove -y python3-urllib3 2>/dev/null || true
|
|
||||||
$PKG_MANAGER reinstall -y certbot python3-certbot-nginx
|
$PKG_MANAGER reinstall -y certbot python3-certbot-nginx
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -1445,10 +1637,9 @@ deploy_acme_letsencrypt() {
|
|||||||
|
|
||||||
print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
|
print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
|
||||||
|
|
||||||
# 使用官方安装方法:直接通过curl管道执行
|
|
||||||
print_info "正在下载并安装..."
|
print_info "正在下载并安装..."
|
||||||
|
|
||||||
if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then
|
if download_and_run_setup_script "https://get.acme.sh" "email=admin@example.com"; then
|
||||||
install_result=$?
|
install_result=$?
|
||||||
print_info "安装脚本执行完成,退出码: $install_result"
|
print_info "安装脚本执行完成,退出码: $install_result"
|
||||||
else
|
else
|
||||||
@@ -1456,9 +1647,6 @@ deploy_acme_letsencrypt() {
|
|||||||
print_error "安装脚本执行失败,退出码: $install_result"
|
print_error "安装脚本执行失败,退出码: $install_result"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 重新加载环境变量
|
|
||||||
source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
|
|
||||||
|
|
||||||
# 等待文件系统同步
|
# 等待文件系统同步
|
||||||
print_info "等待安装完成..."
|
print_info "等待安装完成..."
|
||||||
sleep 3
|
sleep 3
|
||||||
@@ -1478,7 +1666,7 @@ deploy_acme_letsencrypt() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -d ~/.acme.sh ]]; then
|
if [[ -d ~/.acme.sh ]]; then
|
||||||
print_info "~/.acme.sh 目录内容:"
|
print_info "${HOME}/.acme.sh 目录内容:"
|
||||||
ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
|
ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
@@ -1626,10 +1814,9 @@ deploy_acme_zerossl() {
|
|||||||
|
|
||||||
print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
|
print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
|
||||||
|
|
||||||
# 使用官方安装方法:直接通过curl管道执行
|
|
||||||
print_info "正在下载并安装..."
|
print_info "正在下载并安装..."
|
||||||
|
|
||||||
if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then
|
if download_and_run_setup_script "https://get.acme.sh" "email=admin@example.com"; then
|
||||||
install_result=$?
|
install_result=$?
|
||||||
print_info "安装脚本执行完成,退出码: $install_result"
|
print_info "安装脚本执行完成,退出码: $install_result"
|
||||||
else
|
else
|
||||||
@@ -1637,9 +1824,6 @@ deploy_acme_zerossl() {
|
|||||||
print_error "安装脚本执行失败,退出码: $install_result"
|
print_error "安装脚本执行失败,退出码: $install_result"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 重新加载环境变量
|
|
||||||
source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
|
|
||||||
|
|
||||||
# 等待文件系统同步
|
# 等待文件系统同步
|
||||||
print_info "等待安装完成..."
|
print_info "等待安装完成..."
|
||||||
sleep 3
|
sleep 3
|
||||||
@@ -1657,7 +1841,7 @@ deploy_acme_zerossl() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -d ~/.acme.sh ]]; then
|
if [[ -d ~/.acme.sh ]]; then
|
||||||
print_info "~/.acme.sh 目录内容:"
|
print_info "${HOME}/.acme.sh 目录内容:"
|
||||||
ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
|
ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
@@ -1785,10 +1969,9 @@ deploy_acme_buypass() {
|
|||||||
|
|
||||||
print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
|
print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
|
||||||
|
|
||||||
# 使用官方安装方法:直接通过curl管道执行
|
|
||||||
print_info "正在下载并安装..."
|
print_info "正在下载并安装..."
|
||||||
|
|
||||||
if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then
|
if download_and_run_setup_script "https://get.acme.sh" "email=admin@example.com"; then
|
||||||
install_result=$?
|
install_result=$?
|
||||||
print_info "安装脚本执行完成,退出码: $install_result"
|
print_info "安装脚本执行完成,退出码: $install_result"
|
||||||
else
|
else
|
||||||
@@ -1796,9 +1979,6 @@ deploy_acme_buypass() {
|
|||||||
print_error "安装脚本执行失败,退出码: $install_result"
|
print_error "安装脚本执行失败,退出码: $install_result"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 重新加载环境变量
|
|
||||||
source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
|
|
||||||
|
|
||||||
# 等待文件系统同步
|
# 等待文件系统同步
|
||||||
print_info "等待安装完成..."
|
print_info "等待安装完成..."
|
||||||
sleep 3
|
sleep 3
|
||||||
@@ -1816,7 +1996,7 @@ deploy_acme_buypass() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -d ~/.acme.sh ]]; then
|
if [[ -d ~/.acme.sh ]]; then
|
||||||
print_info "~/.acme.sh 目录内容:"
|
print_info "${HOME}/.acme.sh 目录内容:"
|
||||||
ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
|
ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
@@ -1928,34 +2108,6 @@ deploy_acme_buypass() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_aliyun_ssl() {
|
|
||||||
print_step "使用阿里云免费证书..."
|
|
||||||
|
|
||||||
print_warning "此功能需要您提供阿里云AccessKey"
|
|
||||||
echo ""
|
|
||||||
read -p "阿里云AccessKey ID: " ALIYUN_ACCESS_KEY_ID < /dev/tty
|
|
||||||
read -p "阿里云AccessKey Secret: " ALIYUN_ACCESS_KEY_SECRET < /dev/tty
|
|
||||||
|
|
||||||
# 这里需要调用阿里云API申请证书
|
|
||||||
# 暂时返回失败,提示用户使用其他方案
|
|
||||||
print_error "阿里云证书申请功能开发中,请选择其他方案"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
deploy_tencent_ssl() {
|
|
||||||
print_step "使用腾讯云免费证书..."
|
|
||||||
|
|
||||||
print_warning "此功能需要您提供腾讯云SecretKey"
|
|
||||||
echo ""
|
|
||||||
read -p "腾讯云SecretId: " TENCENT_SECRET_ID < /dev/tty
|
|
||||||
read -p "腾讯云SecretKey: " TENCENT_SECRET_KEY < /dev/tty
|
|
||||||
|
|
||||||
# 这里需要调用腾讯云API申请证书
|
|
||||||
# 暂时返回失败,提示用户使用其他方案
|
|
||||||
print_error "腾讯云证书申请功能开发中,请选择其他方案"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
deploy_manual_ssl() {
|
deploy_manual_ssl() {
|
||||||
print_step "使用已有证书..."
|
print_step "使用已有证书..."
|
||||||
|
|
||||||
@@ -2063,9 +2215,9 @@ configure_admin_account() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
local type_count=0
|
local type_count=0
|
||||||
[[ "$ADMIN_PASSWORD" =~ [A-Za-z] ]] && ((type_count++))
|
[[ "$ADMIN_PASSWORD" =~ [A-Za-z] ]] && ((type_count += 1))
|
||||||
[[ "$ADMIN_PASSWORD" =~ [0-9] ]] && ((type_count++))
|
[[ "$ADMIN_PASSWORD" =~ [0-9] ]] && ((type_count += 1))
|
||||||
[[ "$ADMIN_PASSWORD" =~ [^A-Za-z0-9] ]] && ((type_count++))
|
[[ "$ADMIN_PASSWORD" =~ [^A-Za-z0-9] ]] && ((type_count += 1))
|
||||||
if [[ ${type_count} -lt 2 ]]; then
|
if [[ ${type_count} -lt 2 ]]; then
|
||||||
print_error "密码必须包含字母、数字、特殊字符中的至少两种"
|
print_error "密码必须包含字母、数字、特殊字符中的至少两种"
|
||||||
continue
|
continue
|
||||||
@@ -2191,21 +2343,39 @@ create_env_file() {
|
|||||||
ALLOWED_ORIGINS_VALUE="${PROTOCOL}://${DOMAIN}:${PORT_VALUE}"
|
ALLOWED_ORIGINS_VALUE="${PROTOCOL}://${DOMAIN}:${PORT_VALUE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_info "CORS 配置: ${ALLOWED_ORIGINS_VALUE}"
|
PUBLIC_BASE_URL_VALUE="$ALLOWED_ORIGINS_VALUE"
|
||||||
|
ALLOWED_HOSTS_VALUE="$DOMAIN"
|
||||||
|
PUBLIC_PORT_VALUE="$PORT_VALUE"
|
||||||
|
print_info "公开访问地址: ${PUBLIC_BASE_URL_VALUE}"
|
||||||
else
|
else
|
||||||
# IP 模式(开发/测试环境)
|
# IP 模式也必须生成明确的生产来源白名单,否则浏览器 API 请求会被 CORS 拒绝。
|
||||||
# 留空,后端默认允许所有来源(适合开发环境)
|
PUBLIC_IP=${PUBLIC_IP:-$(get_public_ip || true)}
|
||||||
ALLOWED_ORIGINS_VALUE=""
|
if [[ -z "$PUBLIC_IP" ]]; then
|
||||||
|
print_error "无法确定公开访问 IP,不能生成安全的生产配置"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
PUBLIC_URL_HOST=$(format_url_host "$PUBLIC_IP")
|
||||||
|
PORT_VALUE=${HTTP_PORT:-80}
|
||||||
|
if [[ "$PORT_VALUE" == "80" ]]; then
|
||||||
|
PUBLIC_BASE_URL_VALUE="http://${PUBLIC_URL_HOST}"
|
||||||
|
else
|
||||||
|
PUBLIC_BASE_URL_VALUE="http://${PUBLIC_URL_HOST}:${PORT_VALUE}"
|
||||||
|
fi
|
||||||
|
ALLOWED_ORIGINS_VALUE="$PUBLIC_BASE_URL_VALUE"
|
||||||
|
ALLOWED_HOSTS_VALUE="$PUBLIC_IP"
|
||||||
|
PUBLIC_PORT_VALUE="$PORT_VALUE"
|
||||||
COOKIE_SECURE_VALUE="false"
|
COOKIE_SECURE_VALUE="false"
|
||||||
ENFORCE_HTTPS_VALUE="false"
|
ENFORCE_HTTPS_VALUE="false"
|
||||||
print_warning "IP 模式下 CORS 将允许所有来源(仅适合开发环境)"
|
print_info "公开访问地址: ${PUBLIC_BASE_URL_VALUE}"
|
||||||
print_info "生产环境建议使用域名模式"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ADMIN_USERNAME_ENV=$(dotenv_quote "$ADMIN_USERNAME")
|
||||||
|
ADMIN_PASSWORD_ENV=$(dotenv_quote "$ADMIN_PASSWORD")
|
||||||
|
|
||||||
cat > "${PROJECT_DIR}/backend/.env" << EOF
|
cat > "${PROJECT_DIR}/backend/.env" << EOF
|
||||||
# 管理员账号
|
# 管理员账号
|
||||||
ADMIN_USERNAME=${ADMIN_USERNAME}
|
ADMIN_USERNAME=${ADMIN_USERNAME_ENV}
|
||||||
ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
ADMIN_PASSWORD=${ADMIN_PASSWORD_ENV}
|
||||||
|
|
||||||
# JWT密钥
|
# JWT密钥
|
||||||
JWT_SECRET=${JWT_SECRET}
|
JWT_SECRET=${JWT_SECRET}
|
||||||
@@ -2226,6 +2396,10 @@ PORT=${BACKEND_PORT}
|
|||||||
# 环境
|
# 环境
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# 公开访问地址(用于邮件、分享和直链)
|
||||||
|
PUBLIC_BASE_URL=${PUBLIC_BASE_URL_VALUE}
|
||||||
|
ALLOWED_HOSTS=${ALLOWED_HOSTS_VALUE}
|
||||||
|
|
||||||
# 强制HTTPS(生产环境建议开启)
|
# 强制HTTPS(生产环境建议开启)
|
||||||
ENFORCE_HTTPS=${ENFORCE_HTTPS_VALUE}
|
ENFORCE_HTTPS=${ENFORCE_HTTPS_VALUE}
|
||||||
|
|
||||||
@@ -2248,9 +2422,9 @@ COOKIE_SECURE=${COOKIE_SECURE_VALUE}
|
|||||||
# 警告:不要设置为 true,这会信任所有代理,存在 IP/协议伪造风险!
|
# 警告:不要设置为 true,这会信任所有代理,存在 IP/协议伪造风险!
|
||||||
TRUST_PROXY=1
|
TRUST_PROXY=1
|
||||||
|
|
||||||
# 公开端口(nginx监听的端口,用于生成分享链接)
|
# 公开端口(Nginx 监听端口)
|
||||||
# 如果使用标准端口(80/443)或未配置,分享链接将不包含端口号
|
# 如果使用标准端口(80/443)或未配置,分享链接将不包含端口号
|
||||||
PUBLIC_PORT=${HTTP_PORT}
|
PUBLIC_PORT=${PUBLIC_PORT_VALUE}
|
||||||
|
|
||||||
# CSRF 保护(生产环境强烈建议开启)
|
# CSRF 保护(生产环境强烈建议开启)
|
||||||
# 使用 Double Submit Cookie 模式防止跨站请求伪造攻击
|
# 使用 Double Submit Cookie 模式防止跨站请求伪造攻击
|
||||||
@@ -2266,15 +2440,16 @@ LOG_LEVEL=info
|
|||||||
LOG_FORMAT=json
|
LOG_FORMAT=json
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "${PROJECT_DIR}/backend/.env"
|
||||||
|
|
||||||
print_success "配置文件创建完成"
|
print_success "配置文件创建完成"
|
||||||
|
|
||||||
# 显示安全提示
|
# 显示安全提示
|
||||||
if [[ -z "$ALLOWED_ORIGINS_VALUE" ]]; then
|
if [[ "$USE_DOMAIN" != "true" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
print_warning "⚠️ 安全提示:"
|
print_warning "⚠️ 安全提示:"
|
||||||
print_info " 当前配置允许所有域名访问(CORS: *)"
|
print_info " IP 模式使用明文 HTTP,管理员密码和会话不受 TLS 保护"
|
||||||
print_info " 这仅适合开发环境,生产环境存在安全风险"
|
print_info " 正式对外服务建议改用域名 + HTTPS 模式"
|
||||||
print_info " 建议在生产环境使用域名模式部署"
|
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
@@ -3049,6 +3224,16 @@ health_check() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 进程在线和端口监听并不代表应用已完成初始化,继续验证真实健康接口。
|
||||||
|
if curl -fsS --connect-timeout 3 --max-time 10 \
|
||||||
|
"http://127.0.0.1:${BACKEND_PORT}/api/health" > /dev/null; then
|
||||||
|
print_success "后端健康接口响应正常"
|
||||||
|
else
|
||||||
|
print_error "后端健康接口响应异常"
|
||||||
|
print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 检查数据库
|
# 检查数据库
|
||||||
if [[ -f "${PROJECT_DIR}/backend/data/database.db" ]]; then
|
if [[ -f "${PROJECT_DIR}/backend/data/database.db" ]]; then
|
||||||
print_success "数据库初始化成功"
|
print_success "数据库初始化成功"
|
||||||
@@ -3072,7 +3257,7 @@ health_check() {
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
print_completion() {
|
print_completion() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${GREEN}"
|
echo -e "${GREEN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -3098,11 +3283,13 @@ print_completion() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "服务器IP")
|
PUBLIC_IP=$(get_public_ip || true)
|
||||||
|
PUBLIC_IP=${PUBLIC_IP:-服务器IP}
|
||||||
|
PUBLIC_URL_HOST=$(format_url_host "$PUBLIC_IP")
|
||||||
if [[ "$HTTP_PORT" == "80" ]]; then
|
if [[ "$HTTP_PORT" == "80" ]]; then
|
||||||
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}"
|
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_URL_HOST}"
|
||||||
else
|
else
|
||||||
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}:${HTTP_PORT}"
|
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_URL_HOST}:${HTTP_PORT}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -3158,7 +3345,7 @@ print_completion() {
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
print_uninstall_banner() {
|
print_uninstall_banner() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${RED}"
|
echo -e "${RED}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -3395,7 +3582,7 @@ uninstall_check_residual() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print_uninstall_completion() {
|
print_uninstall_completion() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${GREEN}"
|
echo -e "${GREEN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -3434,7 +3621,7 @@ print_uninstall_completion() {
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
print_update_banner() {
|
print_update_banner() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${BLUE}"
|
echo -e "${BLUE}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -3870,7 +4057,7 @@ update_migrate_database() {
|
|||||||
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
# 备份原配置
|
# 备份原配置
|
||||||
cp .env .env.backup.$(date +%Y%m%d_%H%M%S)
|
cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
# 修复CORS配置
|
# 修复CORS配置
|
||||||
sed -i "s|^ALLOWED_ORIGINS=.*|ALLOWED_ORIGINS=${FIXED_CORS}|" .env
|
sed -i "s|^ALLOWED_ORIGINS=.*|ALLOWED_ORIGINS=${FIXED_CORS}|" .env
|
||||||
@@ -3946,7 +4133,7 @@ update_check_version() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print_update_completion() {
|
print_update_completion() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${GREEN}"
|
echo -e "${GREEN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -4152,10 +4339,17 @@ main() {
|
|||||||
# 检查root权限
|
# 检查root权限
|
||||||
check_root
|
check_root
|
||||||
|
|
||||||
|
# 安装过程需要读取软件源、端口和管理员密码,必须有真实控制终端。
|
||||||
|
if ! has_controlling_terminal; then
|
||||||
|
print_error "安装需要交互终端,当前环境无法读取 /dev/tty"
|
||||||
|
print_info "请先下载脚本,再在 SSH 终端中执行: bash install.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 如果没有通过命令行参数指定模式,则显示交互式选择
|
# 如果没有通过命令行参数指定模式,则显示交互式选择
|
||||||
if [[ "$MODE" == "install" ]] && [[ "$1" != "--skip-mode-select" ]]; then
|
if [[ "$MODE" == "install" ]] && [[ "$1" != "--skip-mode-select" ]]; then
|
||||||
# 检测是否可以使用交互式输入
|
# 检测是否可以使用交互式输入
|
||||||
if [[ -t 0 ]] || [[ -c /dev/tty ]]; then
|
if has_controlling_terminal; then
|
||||||
print_step "请选择操作模式"
|
print_step "请选择操作模式"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}[1]${NC} 安装/部署 玩玩云"
|
echo -e "${GREEN}[1]${NC} 安装/部署 玩玩云"
|
||||||
@@ -4324,7 +4518,7 @@ uninstall_main() {
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
print_repair_banner() {
|
print_repair_banner() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${BLUE}"
|
echo -e "${BLUE}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -4525,7 +4719,7 @@ repair_verify_services() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print_repair_completion() {
|
print_repair_completion() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${GREEN}"
|
echo -e "${GREEN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -4563,11 +4757,13 @@ print_repair_completion() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "服务器IP")
|
PUBLIC_IP=$(get_public_ip || true)
|
||||||
|
PUBLIC_IP=${PUBLIC_IP:-服务器IP}
|
||||||
|
PUBLIC_URL_HOST=$(format_url_host "$PUBLIC_IP")
|
||||||
if [[ "$HTTP_PORT" == "80" ]]; then
|
if [[ "$HTTP_PORT" == "80" ]]; then
|
||||||
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}"
|
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_URL_HOST}"
|
||||||
else
|
else
|
||||||
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}:${HTTP_PORT}"
|
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_URL_HOST}:${HTTP_PORT}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
@@ -4628,7 +4824,7 @@ repair_main() {
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
print_ssl_banner() {
|
print_ssl_banner() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${GREEN}"
|
echo -e "${GREEN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -4782,7 +4978,7 @@ ssl_configure_domain() {
|
|||||||
# 验证域名解析
|
# 验证域名解析
|
||||||
print_info "正在验证域名解析..."
|
print_info "正在验证域名解析..."
|
||||||
DOMAIN_IP=$(dig +short "$DOMAIN" 2>/dev/null | tail -n1 || nslookup "$DOMAIN" 2>/dev/null | grep -A1 "Name:" | tail -1 | awk '{print $2}')
|
DOMAIN_IP=$(dig +short "$DOMAIN" 2>/dev/null | tail -n1 || nslookup "$DOMAIN" 2>/dev/null | grep -A1 "Name:" | tail -1 | awk '{print $2}')
|
||||||
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "")
|
PUBLIC_IP=$(get_public_ip || true)
|
||||||
|
|
||||||
if [[ -n "$DOMAIN_IP" ]] && [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then
|
if [[ -n "$DOMAIN_IP" ]] && [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then
|
||||||
print_success "域名已正确解析到当前服务器IP"
|
print_success "域名已正确解析到当前服务器IP"
|
||||||
@@ -4818,10 +5014,6 @@ ssl_choose_method() {
|
|||||||
echo -e "${GREEN}[5]${NC} acme.sh + Buypass"
|
echo -e "${GREEN}[5]${NC} acme.sh + Buypass"
|
||||||
echo " - 挪威免费CA,有效期180天"
|
echo " - 挪威免费CA,有效期180天"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}【云服务商证书】${NC}"
|
|
||||||
echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)"
|
|
||||||
echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)"
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}【其他选项】${NC}"
|
echo -e "${YELLOW}【其他选项】${NC}"
|
||||||
echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)"
|
echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)"
|
||||||
echo -e "${GREEN}[8]${NC} 移除HTTPS配置 (改回HTTP)"
|
echo -e "${GREEN}[8]${NC} 移除HTTPS配置 (改回HTTP)"
|
||||||
@@ -4831,7 +5023,7 @@ ssl_choose_method() {
|
|||||||
while true; do
|
while true; do
|
||||||
read -p "请输入选项 [0-8]: " ssl_choice < /dev/tty
|
read -p "请输入选项 [0-8]: " ssl_choice < /dev/tty
|
||||||
case $ssl_choice in
|
case $ssl_choice in
|
||||||
1|2|3|4|5|6|7)
|
1|2|3|5|7)
|
||||||
SSL_METHOD=$ssl_choice
|
SSL_METHOD=$ssl_choice
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
@@ -4971,7 +5163,7 @@ ssl_verify_deployment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print_ssl_completion() {
|
print_ssl_completion() {
|
||||||
clear
|
clear_screen
|
||||||
echo -e "${GREEN}"
|
echo -e "${GREEN}"
|
||||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ║"
|
echo "║ ║"
|
||||||
@@ -5089,5 +5281,5 @@ elif [[ "$MODE" == "repair" ]]; then
|
|||||||
elif [[ "$MODE" == "ssl" ]]; then
|
elif [[ "$MODE" == "ssl" ]]; then
|
||||||
ssl_main
|
ssl_main
|
||||||
else
|
else
|
||||||
main
|
main "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user