Files
AI-Chat/public/install.php

339 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// 检查是否已安装
$configFile = __DIR__ . '/../config/db-config.json';
if (file_exists($configFile)) {
header('Location: /login.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Chat - 安装向导</title>
<link rel="stylesheet" href="/assets/css/style.css">
<style>
.install-container { max-width: 700px; margin: 40px auto; padding: 0 20px; }
.step-content { display: none; background: var(--bg-card); border-radius: var(--radius); padding: 24px; }
.step-content.active { display: block; }
.check-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border-color); }
.check-item .status { font-weight: bold; }
.check-item .status.pass { color: var(--success); }
.check-item .status.fail { color: var(--danger); }
.provider-item { background: var(--bg-secondary); padding: 16px; border-radius: var(--radius); margin-bottom: 12px; }
.provider-item .form-group { margin-bottom: 8px; }
h1 { text-align: center; margin-bottom: 30px; color: var(--primary); }
</style>
</head>
<body>
<div class="install-container">
<h1>🤖 AI Chat 安装向导</h1>
<!-- 步骤指示器 -->
<ul class="step-indicator">
<li class="step active" data-step="1">1. 环境检查</li>
<li class="step" data-step="2">2. 数据库配置</li>
<li class="step" data-step="3">3. 应用配置</li>
<li class="step" data-step="4">4. 管理员账户</li>
<li class="step" data-step="5">5. AI供应商</li>
</ul>
<!-- 步骤1环境检查 -->
<div class="step-content active" id="step1">
<h2>环境检查</h2>
<div id="envChecks">
<!-- 由 JS 动态填充 -->
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="InstallWizard.nextStep()">下一步</button>
</div>
</div>
<!-- 步骤2数据库配置 -->
<div class="step-content" id="step2">
<h2>数据库配置</h2>
<div class="form-group">
<label>主机地址</label>
<input type="text" id="dbHost" value="127.0.0.1" placeholder="数据库主机">
</div>
<div class="form-group">
<label>端口</label>
<input type="number" id="dbPort" value="3306" placeholder="数据库端口">
</div>
<div class="form-group">
<label>数据库名</label>
<input type="text" id="dbName" placeholder="数据库名称">
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="dbUser" placeholder="数据库用户名">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="dbPassword" placeholder="数据库密码">
</div>
<div id="dbTestResult"></div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="InstallWizard.prevStep()">上一步</button>
<button class="btn btn-secondary" onclick="InstallWizard.testDb()">测试连接</button>
<button class="btn btn-primary" onclick="InstallWizard.nextStep()">下一步</button>
</div>
</div>
<!-- 步骤3应用配置 -->
<div class="step-content" id="step3">
<h2>应用配置</h2>
<div class="form-group">
<label>JWT 密钥</label>
<div style="display:flex;gap:8px;">
<input type="text" id="jwtSecret" placeholder="留空则自动生成" style="flex:1;">
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('jwtSecret').value=InstallWizard.generateSecret()">自动生成</button>
</div>
</div>
<div class="form-group">
<label>JWT 过期时间(秒)</label>
<input type="number" id="jwtExpiry" value="86400" placeholder="默认 8640024小时">
</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="InstallWizard.prevStep()">上一步</button>
<button class="btn btn-primary" onclick="InstallWizard.nextStep()">下一步</button>
</div>
</div>
<!-- 步骤4管理员账户 -->
<div class="step-content" id="step4">
<h2>创建管理员账户</h2>
<div class="form-group">
<label>用户名</label>
<input type="text" id="adminUsername" placeholder="管理员用户名">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="adminPassword" placeholder="至少6位密码">
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" id="adminPasswordConfirm" placeholder="再次输入密码">
</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="InstallWizard.prevStep()">上一步</button>
<button class="btn btn-primary" onclick="InstallWizard.nextStep()">下一步</button>
</div>
</div>
<!-- 步骤5AI供应商 -->
<div class="step-content" id="step5">
<h2>AI 供应商配置</h2>
<p style="color:var(--text-secondary);margin-bottom:16px;">至少配置一个 AI 供应商</p>
<div id="providerList">
<!-- 由 JS 动态管理 -->
</div>
<button class="btn btn-secondary" onclick="InstallWizard.addProvider()" style="margin-top:8px;">+ 添加供应商</button>
<div id="installResult"></div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="InstallWizard.prevStep()">上一步</button>
<button class="btn btn-primary" id="installBtn" onclick="InstallWizard.runInstall()">完成安装</button>
</div>
</div>
</div>
<script>
const InstallWizard = {
currentStep: 1,
totalSteps: 5,
init() {
this.checkEnv();
this.addProvider(); // 默认添加一个供应商表单
},
checkEnv() {
const checks = [
{ name: 'PHP 版本 >= 8.0', pass: <?php echo version_compare(PHP_VERSION, '8.0.0', '>=') ? 'true' : 'false'; ?> },
{ name: 'PDO 扩展', pass: <?php echo extension_loaded('pdo') ? 'true' : 'false'; ?> },
{ name: 'cURL 扩展', pass: <?php echo extension_loaded('curl') ? 'true' : 'false'; ?> },
{ name: 'uploads/ 目录可写', pass: <?php echo is_writable(__DIR__ . '/../uploads') ? 'true' : 'false'; ?> },
{ name: 'config/ 目录可写', pass: <?php echo is_writable(__DIR__ . '/../config') ? 'true' : 'false'; ?> }
];
const container = document.getElementById('envChecks');
container.innerHTML = checks.map(c => `
<div class="check-item">
<span>${c.name}</span>
<span class="status ${c.pass ? 'pass' : 'fail'}">${c.pass ? '✓ 通过' : '✗ 未通过'}</span>
</div>
`).join('');
},
nextStep() {
if (this.currentStep === 4) {
// 验证管理员密码
const pwd = document.getElementById('adminPassword').value;
const confirm = document.getElementById('adminPasswordConfirm').value;
const username = document.getElementById('adminUsername').value;
if (!username) { alert('请输入管理员用户名'); return; }
if (pwd.length < 6) { alert('密码至少6位'); return; }
if (pwd !== confirm) { alert('两次密码不一致'); return; }
}
if (this.currentStep < this.totalSteps) {
this.currentStep++;
this.updateSteps();
}
},
prevStep() {
if (this.currentStep > 1) {
this.currentStep--;
this.updateSteps();
}
},
updateSteps() {
document.querySelectorAll('.step-content').forEach((el, i) => {
el.classList.toggle('active', i + 1 === this.currentStep);
});
document.querySelectorAll('.step-indicator .step').forEach((el, i) => {
el.classList.remove('active', 'completed');
if (i + 1 === this.currentStep) el.classList.add('active');
if (i + 1 < this.currentStep) el.classList.add('completed');
});
},
async testDb() {
const result = document.getElementById('dbTestResult');
try {
const response = await fetch('/api/install/test-db', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
host: document.getElementById('dbHost').value,
port: parseInt(document.getElementById('dbPort').value),
user: document.getElementById('dbUser').value,
password: document.getElementById('dbPassword').value,
database: document.getElementById('dbName').value
})
});
const data = await response.json();
result.innerHTML = `<div class="alert ${data.success ? 'alert-success' : 'alert-error'}">${data.message}</div>`;
} catch (err) {
result.innerHTML = '<div class="alert alert-error">连接失败: ' + err.message + '</div>';
}
},
addProvider() {
const list = document.getElementById('providerList');
const index = list.children.length;
const html = `
<div class="provider-item" data-index="${index}">
<div class="form-group">
<label>供应商名称</label>
<input type="text" class="provider-name" placeholder="如OpenAI、DeepSeek">
</div>
<div class="form-group">
<label>API URL</label>
<input type="text" class="provider-url" placeholder="如https://api.openai.com">
</div>
<div class="form-group">
<label>API Key</label>
<input type="password" class="provider-key" placeholder="API 密钥">
</div>
<div class="form-group">
<label>可用模型(逗号分隔)</label>
<input type="text" class="provider-models" placeholder="如gpt-3.5-turbo, gpt-4">
</div>
<div class="form-group">
<label>供应商类型</label>
<select class="provider-type">
<option value="newapi">OpenAI 兼容</option>
<option value="openai">OpenAI 官方</option>
<option value="claude">Claude (Anthropic)</option>
</select>
</div>
${index > 0 ? '<button class="btn btn-danger btn-sm" onclick="this.parentElement.remove()">删除</button>' : ''}
</div>
`;
list.insertAdjacentHTML('beforeend', html);
},
generateSecret() {
const chars = '0123456789abcdef';
let result = '';
for (let i = 0; i < 64; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
},
async runInstall() {
const btn = document.getElementById('installBtn');
const result = document.getElementById('installResult');
// 收集供应商数据
const providers = [];
document.querySelectorAll('.provider-item').forEach(item => {
const models = item.querySelector('.provider-models').value.split(',').map(m => m.trim()).filter(m => m);
providers.push({
name: item.querySelector('.provider-name').value,
apiUrl: item.querySelector('.provider-url').value,
apiKey: item.querySelector('.provider-key').value,
models: models,
type: item.querySelector('.provider-type').value,
enabled: true
});
});
if (providers.length === 0 || !providers[0].name || !providers[0].apiKey) {
alert('请至少配置一个完整的供应商');
return;
}
btn.disabled = true;
btn.textContent = '安装中...';
const setupData = {
username: document.getElementById('adminUsername').value,
password: document.getElementById('adminPassword').value,
dbConfig: {
host: document.getElementById('dbHost').value,
port: parseInt(document.getElementById('dbPort').value),
user: document.getElementById('dbUser').value,
password: document.getElementById('dbPassword').value,
database: document.getElementById('dbName').value
},
appConfig: {
jwtSecret: document.getElementById('jwtSecret').value || undefined,
jwtExpiry: parseInt(document.getElementById('jwtExpiry').value) || 86400
},
providers: providers
};
try {
const response = await fetch('/api/install/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(setupData)
});
const data = await response.json();
if (data.success) {
result.innerHTML = '<div class="alert alert-success">✓ 安装成功!正在跳转到登录页...</div>';
setTimeout(() => { window.location.href = '/login.php'; }, 2000);
} else {
result.innerHTML = '<div class="alert alert-error">安装失败: ' + data.message + '</div>';
btn.disabled = false;
btn.textContent = '完成安装';
}
} catch (err) {
result.innerHTML = '<div class="alert alert-error">安装失败: ' + err.message + '</div>';
btn.disabled = false;
btn.textContent = '完成安装';
}
}
};
InstallWizard.init();
</script>
</body>
</html>