This commit is contained in:
parent
1dcedf3150
commit
ddd85bc176
|
|
@ -30,6 +30,15 @@ dingtalk:
|
|||
access_token: ed3533e05cf13e090c098436ee6cd52b2adfa2d85b5b2b9da1ae2bccdaecb8f3
|
||||
secret: SEC66372694e16e7e931f53aefb4b847b7fb6c42350a10f0f27fbf4151785353261
|
||||
|
||||
# 数据库配置(用于数据库管理页面)
|
||||
database:
|
||||
container_name: ruoyi-mysql # MySQL 容器名称
|
||||
host: localhost
|
||||
port: 3306
|
||||
user: root
|
||||
password: password
|
||||
charset: utf8mb4
|
||||
|
||||
# 基础设施服务配置(只部署一次)
|
||||
infrastructure:
|
||||
- name: ruoyi-mysql
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
from flask import Flask, request, jsonify, render_template_string
|
||||
|
||||
# 数据库相关导入
|
||||
try:
|
||||
import pymysql
|
||||
pymysql.install_as_MySQLdb()
|
||||
PYMYSQL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYMYSQL_AVAILABLE = False
|
||||
print("警告: pymysql 未安装,数据库管理功能将不可用。请运行: pip install pymysql")
|
||||
|
||||
# 添加当前目录到 Python 路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
|
@ -862,7 +871,7 @@ class DeploymentServer:
|
|||
<h3>📖 使用说明</h3>
|
||||
<p><strong>触发部署:</strong>点击下方表格中的"部署链接",或直接访问 <code>http://IP:9999/项目名称</code></p>
|
||||
<p><strong>示例:</strong><code>http://IP:9999/tuoheng-device</code> 触发 tuoheng-device 项目部署</p>
|
||||
<p><strong>查看日志:</strong><a href="/logs" style="color: #1976d2; font-weight: bold;">点击这里查看部署日志</a> | <a href="/containers" style="color: #1976d2; font-weight: bold;">查看容器日志</a></p>
|
||||
<p><strong>查看日志:</strong><a href="/logs" style="color: #1976d2; font-weight: bold;">点击这里查看部署日志</a> | <a href="/containers" style="color: #1976d2; font-weight: bold;">查看容器日志</a> | <a href="/database" style="color: #1976d2; font-weight: bold;">数据库管理</a></p>
|
||||
</div>
|
||||
|
||||
<h2>🌐 服务访问入口</h2>
|
||||
|
|
@ -1466,11 +1475,553 @@ class DeploymentServer:
|
|||
except Exception as e:
|
||||
return jsonify({'logs': [], 'error': str(e)})
|
||||
|
||||
@self.app.route('/database')
|
||||
def view_database():
|
||||
"""数据库管理页面"""
|
||||
if not PYMYSQL_AVAILABLE:
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>数据库管理 - 错误</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>数据库管理功能不可用</h1>
|
||||
<p>pymysql 模块未安装,请运行: pip install pymysql</p>
|
||||
<a href="/">返回首页</a>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# 获取数据库配置
|
||||
db_config = self.monitor.config.get('database', {})
|
||||
container_name = db_config.get('container_name', 'ruoyi-mysql')
|
||||
|
||||
html = f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>数据库管理</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.header {{
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.header h1 {{
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}}
|
||||
.back-link {{
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
}}
|
||||
.back-link:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
.panel {{
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}}
|
||||
.panel h2 {{
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #1976d2;
|
||||
padding-bottom: 10px;
|
||||
}}
|
||||
.db-list {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.db-item {{
|
||||
padding: 15px;
|
||||
background-color: #e3f2fd;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.db-item:hover {{
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}}
|
||||
.db-item.active {{
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}}
|
||||
.table-list {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.table-item {{
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.table-item:hover {{
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}}
|
||||
.sql-editor {{
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
}}
|
||||
.btn {{
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}}
|
||||
.btn:hover {{
|
||||
background-color: #1565c0;
|
||||
}}
|
||||
.btn-success {{
|
||||
background-color: #4caf50;
|
||||
}}
|
||||
.btn-success:hover {{
|
||||
background-color: #45a049;
|
||||
}}
|
||||
.result-container {{
|
||||
margin-top: 20px;
|
||||
overflow-x: auto;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: white;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border: 1px solid #ddd;
|
||||
}}
|
||||
th {{
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}}
|
||||
tr:hover {{
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.loading {{
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
}}
|
||||
.error {{
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 15px;
|
||||
border-radius: 3px;
|
||||
margin-top: 10px;
|
||||
}}
|
||||
.success {{
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
padding: 15px;
|
||||
border-radius: 3px;
|
||||
margin-top: 10px;
|
||||
}}
|
||||
.info {{
|
||||
background-color: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🗄️ 数据库管理 - {container_name}</h1>
|
||||
<a href="/" class="back-link">← 返回首页</a>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<strong>说明:</strong>
|
||||
<ul style="margin: 10px 0;">
|
||||
<li>点击数据库名称查看该数据库的所有表</li>
|
||||
<li>点击表名查看表数据(默认显示前100条)</li>
|
||||
<li>可以在SQL编辑器中执行自定义SQL查询</li>
|
||||
<li>⚠️ 请谨慎执行修改数据的SQL语句</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>📚 数据库列表</h2>
|
||||
<div id="dbList" class="db-list">
|
||||
<div class="loading">正在加载数据库列表...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="tablePanel" style="display: none;">
|
||||
<h2>📋 表列表 - <span id="currentDb"></span></h2>
|
||||
<div id="tableList" class="table-list">
|
||||
<div class="loading">正在加载表列表...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>💻 SQL 查询</h2>
|
||||
<div>
|
||||
<label for="sqlEditor"><strong>SQL 语句:</strong></label>
|
||||
<textarea id="sqlEditor" class="sql-editor" placeholder="输入 SQL 查询语句,例如:SELECT * FROM table_name LIMIT 10"></textarea>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<button class="btn btn-success" onclick="executeSQL()">▶ 执行查询</button>
|
||||
<button class="btn" onclick="clearSQL()">🗑️ 清空</button>
|
||||
</div>
|
||||
<div id="sqlMessage"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="resultPanel" style="display: none;">
|
||||
<h2>📊 查询结果</h2>
|
||||
<div id="resultContainer" class="result-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentDatabase = null;
|
||||
|
||||
// 加载数据库列表
|
||||
function loadDatabases() {{
|
||||
fetch('/api/database/list')
|
||||
.then(response => response.json())
|
||||
.then(data => {{
|
||||
const dbList = document.getElementById('dbList');
|
||||
if (data.success && data.databases) {{
|
||||
dbList.innerHTML = data.databases.map(db =>
|
||||
`<div class="db-item" onclick="selectDatabase('${{db}}')">${{db}}</div>`
|
||||
).join('');
|
||||
|
||||
// 自动选择 ry-cloud 数据库
|
||||
if (data.databases.includes('ry-cloud')) {{
|
||||
selectDatabase('ry-cloud');
|
||||
}}
|
||||
}} else {{
|
||||
dbList.innerHTML = '<div class="error">加载失败: ' + (data.error || '未知错误') + '</div>';
|
||||
}}
|
||||
}})
|
||||
.catch(error => {{
|
||||
document.getElementById('dbList').innerHTML =
|
||||
'<div class="error">加载失败: ' + error.message + '</div>';
|
||||
}});
|
||||
}}
|
||||
|
||||
// 选择数据库
|
||||
function selectDatabase(dbName) {{
|
||||
currentDatabase = dbName;
|
||||
|
||||
// 更新UI
|
||||
document.querySelectorAll('.db-item').forEach(item => {{
|
||||
item.classList.remove('active');
|
||||
if (item.textContent === dbName) {{
|
||||
item.classList.add('active');
|
||||
}}
|
||||
}});
|
||||
|
||||
document.getElementById('currentDb').textContent = dbName;
|
||||
document.getElementById('tablePanel').style.display = 'block';
|
||||
|
||||
// 加载表列表
|
||||
loadTables(dbName);
|
||||
}}
|
||||
|
||||
// 加载表列表
|
||||
function loadTables(dbName) {{
|
||||
const tableList = document.getElementById('tableList');
|
||||
tableList.innerHTML = '<div class="loading">正在加载表列表...</div>';
|
||||
|
||||
fetch(`/api/database/tables?db=${{dbName}}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {{
|
||||
if (data.success && data.tables) {{
|
||||
tableList.innerHTML = data.tables.map(table =>
|
||||
`<div class="table-item" onclick="viewTable('${{dbName}}', '${{table}}')">${{table}}</div>`
|
||||
).join('');
|
||||
}} else {{
|
||||
tableList.innerHTML = '<div class="error">加载失败: ' + (data.error || '未知错误') + '</div>';
|
||||
}}
|
||||
}})
|
||||
.catch(error => {{
|
||||
tableList.innerHTML = '<div class="error">加载失败: ' + error.message + '</div>';
|
||||
}});
|
||||
}}
|
||||
|
||||
// 查看表数据
|
||||
function viewTable(dbName, tableName) {{
|
||||
const sql = `SELECT * FROM \`${{tableName}}\` LIMIT 100`;
|
||||
document.getElementById('sqlEditor').value = sql;
|
||||
executeSQL(dbName);
|
||||
}}
|
||||
|
||||
// 执行SQL
|
||||
function executeSQL(dbName) {{
|
||||
const sql = document.getElementById('sqlEditor').value.trim();
|
||||
const db = dbName || currentDatabase;
|
||||
|
||||
if (!sql) {{
|
||||
showMessage('请输入SQL语句', 'error');
|
||||
return;
|
||||
}}
|
||||
|
||||
if (!db) {{
|
||||
showMessage('请先选择数据库', 'error');
|
||||
return;
|
||||
}}
|
||||
|
||||
showMessage('正在执行查询...', 'info');
|
||||
|
||||
fetch('/api/database/query', {{
|
||||
method: 'POST',
|
||||
headers: {{
|
||||
'Content-Type': 'application/json'
|
||||
}},
|
||||
body: JSON.stringify({{
|
||||
database: db,
|
||||
sql: sql
|
||||
}})
|
||||
}})
|
||||
.then(response => response.json())
|
||||
.then(data => {{
|
||||
if (data.success) {{
|
||||
showMessage(`查询成功!返回 ${{data.rows || 0}} 行数据`, 'success');
|
||||
displayResults(data);
|
||||
}} else {{
|
||||
showMessage('查询失败: ' + (data.error || '未知错误'), 'error');
|
||||
document.getElementById('resultPanel').style.display = 'none';
|
||||
}}
|
||||
}})
|
||||
.catch(error => {{
|
||||
showMessage('查询失败: ' + error.message, 'error');
|
||||
document.getElementById('resultPanel').style.display = 'none';
|
||||
}});
|
||||
}}
|
||||
|
||||
// 显示查询结果
|
||||
function displayResults(data) {{
|
||||
const resultPanel = document.getElementById('resultPanel');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
|
||||
if (!data.data || data.data.length === 0) {{
|
||||
resultContainer.innerHTML = '<div class="loading">查询成功,但没有返回数据</div>';
|
||||
resultPanel.style.display = 'block';
|
||||
return;
|
||||
}}
|
||||
|
||||
// 获取列名
|
||||
const columns = data.columns || Object.keys(data.data[0]);
|
||||
|
||||
// 生成表格
|
||||
let html = '<table><thead><tr>';
|
||||
columns.forEach(col => {{
|
||||
html += `<th>${{col}}</th>`;
|
||||
}});
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
data.data.forEach(row => {{
|
||||
html += '<tr>';
|
||||
columns.forEach(col => {{
|
||||
const value = row[col];
|
||||
const displayValue = value === null ? '<em style="color: #999;">NULL</em>' :
|
||||
(typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
html += `<td>${{displayValue}}</td>`;
|
||||
}});
|
||||
html += '</tr>';
|
||||
}});
|
||||
|
||||
html += '</tbody></table>';
|
||||
resultContainer.innerHTML = html;
|
||||
resultPanel.style.display = 'block';
|
||||
|
||||
// 滚动到结果区域
|
||||
resultPanel.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
|
||||
}}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type) {{
|
||||
const messageDiv = document.getElementById('sqlMessage');
|
||||
messageDiv.innerHTML = `<div class="${{type}}">${{message}}</div>`;
|
||||
|
||||
if (type === 'success' || type === 'info') {{
|
||||
setTimeout(() => {{
|
||||
messageDiv.innerHTML = '';
|
||||
}}, 3000);
|
||||
}}
|
||||
}}
|
||||
|
||||
// 清空SQL
|
||||
function clearSQL() {{
|
||||
document.getElementById('sqlEditor').value = '';
|
||||
document.getElementById('sqlMessage').innerHTML = '';
|
||||
}}
|
||||
|
||||
// 页面加载时加载数据库列表
|
||||
loadDatabases();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
return html
|
||||
|
||||
@self.app.route('/api/database/list')
|
||||
def get_database_list():
|
||||
"""获取数据库列表API"""
|
||||
if not PYMYSQL_AVAILABLE:
|
||||
return jsonify({'success': False, 'error': 'pymysql 未安装'})
|
||||
|
||||
try:
|
||||
db_config = self.monitor.config.get('database', {})
|
||||
container_name = db_config.get('container_name', 'ruoyi-mysql')
|
||||
|
||||
# 通过 docker exec 连接到容器内的 MySQL
|
||||
cmd = f"docker exec {container_name} mysql -uroot -ppassword -e 'SHOW DATABASES;'"
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode != 0:
|
||||
return jsonify({'success': False, 'error': f'连接数据库失败: {result.stderr}'})
|
||||
|
||||
# 解析数据库列表
|
||||
databases = []
|
||||
for line in result.stdout.strip().split('\n')[1:]: # 跳过第一行标题
|
||||
db_name = line.strip()
|
||||
if db_name and db_name not in ['information_schema', 'performance_schema', 'mysql', 'sys']:
|
||||
databases.append(db_name)
|
||||
|
||||
return jsonify({'success': True, 'databases': databases})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@self.app.route('/api/database/tables')
|
||||
def get_table_list():
|
||||
"""获取表列表API"""
|
||||
if not PYMYSQL_AVAILABLE:
|
||||
return jsonify({'success': False, 'error': 'pymysql 未安装'})
|
||||
|
||||
try:
|
||||
db_name = request.args.get('db')
|
||||
if not db_name:
|
||||
return jsonify({'success': False, 'error': '缺少数据库名称'})
|
||||
|
||||
db_config = self.monitor.config.get('database', {})
|
||||
container_name = db_config.get('container_name', 'ruoyi-mysql')
|
||||
|
||||
# 通过 docker exec 连接到容器内的 MySQL
|
||||
cmd = f"docker exec {container_name} mysql -uroot -ppassword -e 'USE {db_name}; SHOW TABLES;'"
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode != 0:
|
||||
return jsonify({'success': False, 'error': f'查询表列表失败: {result.stderr}'})
|
||||
|
||||
# 解析表列表
|
||||
tables = []
|
||||
for line in result.stdout.strip().split('\n')[1:]: # 跳过第一行标题
|
||||
table_name = line.strip()
|
||||
if table_name:
|
||||
tables.append(table_name)
|
||||
|
||||
return jsonify({'success': True, 'tables': tables})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@self.app.route('/api/database/query', methods=['POST'])
|
||||
def execute_query():
|
||||
"""执行SQL查询API"""
|
||||
if not PYMYSQL_AVAILABLE:
|
||||
return jsonify({'success': False, 'error': 'pymysql 未安装'})
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
database = data.get('database')
|
||||
sql = data.get('sql', '').strip()
|
||||
|
||||
if not database or not sql:
|
||||
return jsonify({'success': False, 'error': '缺少必要参数'})
|
||||
|
||||
db_config = self.monitor.config.get('database', {})
|
||||
container_name = db_config.get('container_name', 'ruoyi-mysql')
|
||||
|
||||
# 转义SQL中的单引号
|
||||
sql_escaped = sql.replace("'", "'\\''")
|
||||
|
||||
# 通过 docker exec 执行 SQL
|
||||
cmd = f"docker exec {container_name} mysql -uroot -ppassword {database} -e '{sql_escaped}' --batch"
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode != 0:
|
||||
return jsonify({'success': False, 'error': f'SQL执行失败: {result.stderr}'})
|
||||
|
||||
# 解析查询结果
|
||||
lines = result.stdout.strip().split('\n')
|
||||
if not lines or not lines[0]:
|
||||
return jsonify({'success': True, 'data': [], 'columns': [], 'rows': 0})
|
||||
|
||||
# 第一行是列名
|
||||
columns = lines[0].split('\t')
|
||||
|
||||
# 解析数据行
|
||||
data_rows = []
|
||||
for line in lines[1:]:
|
||||
if line:
|
||||
values = line.split('\t')
|
||||
row_dict = {}
|
||||
for i, col in enumerate(columns):
|
||||
row_dict[col] = values[i] if i < len(values) else None
|
||||
data_rows.append(row_dict)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': data_rows,
|
||||
'columns': columns,
|
||||
'rows': len(data_rows)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@self.app.route('/<project_name>')
|
||||
def deploy_project(project_name):
|
||||
"""触发项目部署"""
|
||||
# 避免与其他路由冲突
|
||||
if project_name in ['logs', 'api', 'containers', 'container-logs']:
|
||||
if project_name in ['logs', 'api', 'containers', 'container-logs', 'database']:
|
||||
return jsonify({'code': 404, 'message': '路径不存在'})
|
||||
|
||||
Logger.info(f"收到HTTP部署请求: {project_name}")
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ if ! python3 -c "import flask" 2>/dev/null; then
|
|||
python3 -m pip install --user --break-system-packages flask 2>/dev/null || \
|
||||
python3 -m pip install --user flask
|
||||
fi
|
||||
if ! python3 -c "import pymysql" 2>/dev/null; then
|
||||
echo "安装 PyMySQL(用于数据库管理功能)..."
|
||||
python3 -m pip install --user --break-system-packages pymysql 2>/dev/null || \
|
||||
python3 -m pip install --user pymysql
|
||||
fi
|
||||
echo "✓ Python 依赖检查完成"
|
||||
|
||||
# 5. 删除已存在的 PM2 进程
|
||||
|
|
|
|||
Loading…
Reference in New Issue