Files
AI-Chat/public/install.php
2026-05-06 16:54:07 +08:00

395 lines
18 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">
</head>
<body>
<div class="install-container">
<!-- 标题区域 -->
<div class="install-header">
<div class="install-logo">
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="12" rx="3"/>
<circle cx="9" cy="10" r="1.5" fill="currentColor" stroke="none"/>
<circle cx="15" cy="10" r="1.5" fill="currentColor" stroke="none"/>
<path d="M8 16v2M16 16v2M12 16v3"/>
</svg>
</div>
<h1>AI Chat 安装向导</h1>
<p class="install-subtitle">跟随引导完成初始配置,快速启用你的 AI 对话平台</p>
</div>
<!-- 步骤指示器 -->
<ul class="step-indicator">
<li class="step active" data-step="1">
<span class="step-num">1</span>
<span class="step-label">环境检查</span>
</li>
<li class="step" data-step="2">
<span class="step-num">2</span>
<span class="step-label">数据库</span>
</li>
<li class="step" data-step="3">
<span class="step-num">3</span>
<span class="step-label">应用配置</span>
</li>
<li class="step" data-step="4">
<span class="step-num">4</span>
<span class="step-label">管理员</span>
</li>
<li class="step" data-step="5">
<span class="step-num">5</span>
<span class="step-label">AI 供应商</span>
</li>
</ul>
<!-- 步骤1环境检查 -->
<div class="step-content active" id="step1">
<div class="step-header">
<h2>环境检查</h2>
<p class="step-desc">检查服务器运行环境是否满足安装要求</p>
</div>
<div id="envChecks" class="check-list">
<!-- 由 JS 动态填充 -->
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="InstallWizard.nextStep()">下一步</button>
</div>
</div>
<!-- 步骤2数据库配置 -->
<div class="step-content" id="step2">
<div class="step-header">
<h2>数据库配置</h2>
<p class="step-desc">填写 MySQL 数据库连接信息</p>
</div>
<div class="form-row">
<div class="form-group" style="flex:2">
<label>主机地址</label>
<input type="text" id="dbHost" value="127.0.0.1" placeholder="数据库主机">
</div>
<div class="form-group" style="flex:1">
<label>端口</label>
<input type="number" id="dbPort" value="3306" placeholder="端口号">
</div>
</div>
<div class="form-group">
<label>数据库名</label>
<input type="text" id="dbName" placeholder="数据库名称">
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>用户名</label>
<input type="text" id="dbUser" placeholder="数据库用户名">
</div>
<div class="form-group" style="flex:1">
<label>密码</label>
<input type="password" id="dbPassword" placeholder="数据库密码">
</div>
</div>
<div id="dbTestResult"></div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="InstallWizard.prevStep()">上一步</button>
<button class="btn btn-outline" onclick="InstallWizard.testDb()">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
测试连接
</button>
<button class="btn btn-primary" onclick="InstallWizard.nextStep()">下一步</button>
</div>
</div>
<!-- 步骤3应用配置 -->
<div class="step-content" id="step3">
<div class="step-header">
<h2>应用配置</h2>
<p class="step-desc">设置 JWT 令牌参数,用于用户认证加密</p>
</div>
<div class="form-group">
<label>JWT 密钥</label>
<div class="input-action-row">
<input type="text" id="jwtSecret" placeholder="留空则自动生成安全密钥">
<button class="btn btn-outline 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小时">
<span class="form-hint">默认 86400 秒24 小时),可根据需要调整</span>
</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">
<div class="step-header">
<h2>创建管理员账户</h2>
<p class="step-desc">设置管理员登录凭据,安装后可在系统中修改</p>
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="adminUsername" placeholder="管理员用户名">
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>密码</label>
<input type="password" id="adminPassword" placeholder="至少6位密码">
</div>
<div class="form-group" style="flex:1">
<label>确认密码</label>
<input type="password" id="adminPasswordConfirm" placeholder="再次输入密码">
</div>
</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">
<div class="step-header">
<h2>AI 供应商配置</h2>
<p class="step-desc">至少配置一个 AI 供应商,安装后可在设置中添加更多</p>
</div>
<div id="providerList">
<!-- 由 JS 动态管理 -->
</div>
<button class="btn btn-outline" onclick="InstallWizard.addProvider()" style="margin-top:12px;width:100%;">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="margin-right:4px;"><path d="M12 5v14M5 12h14"/></svg>
添加供应商
</button>
<div id="installResult" style="margin-top:16px;"></div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="InstallWizard.prevStep()">上一步</button>
<button class="btn btn-primary btn-lg" id="installBtn" onclick="InstallWizard.runInstall()">完成安装</button>
</div>
</div>
<!-- 底部版本信息 -->
<div class="install-footer">
AI Chat &middot; v1.0
</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 ${c.pass ? 'pass' : 'fail'}">
<span class="check-name">${c.name}</span>
<span class="check-status">
${c.pass
? '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> 通过'
: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg> 未通过'
}
</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="provider-item-header">
<span class="provider-item-title">供应商 #${index + 1}</span>
${index > 0 ? '<button class="btn btn-danger btn-sm" onclick="this.closest(\'.provider-item\').remove()">删除</button>' : ''}
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label>供应商名称</label>
<input type="text" class="provider-name" placeholder="如OpenAI、DeepSeek">
</div>
<div class="form-group" style="flex:1">
<label>供应商类型</label>
<select class="provider-type">
<option value="newapi">OpenAI 兼容</option>
<option value="openai">OpenAI 官方</option>
<option value="claude">Claude (Anthropic)</option>
</select>
</div>
</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>
`;
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>