diff --git a/app/Views/chat.php b/app/Views/chat.php
index 9f2ad22..8be832a 100644
--- a/app/Views/chat.php
+++ b/app/Views/chat.php
@@ -31,15 +31,15 @@
- 思考
+ 思考
-
-
+
+
设置
@@ -56,7 +56,7 @@
-
-
diff --git a/app/Views/config.php b/app/Views/config.php
index 1d475c6..786f18a 100644
--- a/app/Views/config.php
+++ b/app/Views/config.php
@@ -1,11 +1,11 @@
@@ -16,7 +16,7 @@
-
+
@@ -25,7 +25,7 @@
- 添加自定义人格
+ 添加自定义人格
-
+
-
+
diff --git a/assets/css/style.css b/assets/css/style.css
index 119d6ac..eb852e9 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -649,3 +649,125 @@ body {
font-size: 22px;
}
}
+
+/* ========================================
+ 工具类
+ ======================================== */
+.hidden { display: none !important; }
+.ml-auto { margin-left: auto; }
+.mt-sm { margin-top: 12px; }
+.mt-md { margin-top: 16px; }
+.mt-lg { margin-top: 24px; }
+.full-width { width: 100%; }
+.text-muted { color: var(--text-secondary); }
+.text-xs { font-size: 12px; }
+.text-center { text-align: center; }
+.col-flex-2 { flex: 2; }
+.col-flex-1 { flex: 1; }
+
+/* 内联 SVG 图标对齐 */
+.icon-inline {
+ vertical-align: -2px;
+ margin-right: 4px;
+}
+
+.icon-inline-md {
+ vertical-align: -4px;
+ margin-right: 8px;
+}
+
+/* ========================================
+ 聊天页工具栏组件
+ ======================================== */
+.toolbar-think-label {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.upload-btn {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 4px;
+}
+
+.settings-link {
+ margin-left: auto;
+}
+
+/* ========================================
+ 配置页组件
+ ======================================== */
+.config-add-btn {
+ margin-top: 12px;
+}
+
+.config-section h3 {
+ margin-top: 20px;
+ margin-bottom: 16px;
+ font-size: 15px;
+ font-weight: 600;
+}
+
+.config-empty {
+ color: var(--text-secondary);
+}
+
+.config-provider-card {
+ background: var(--bg-secondary);
+ padding: 16px;
+ border-radius: var(--radius);
+ margin-bottom: 12px;
+}
+
+.config-provider-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.config-provider-header strong {
+ font-size: 15px;
+}
+
+.config-provider-header .toggle-switch {
+ margin-right: 8px;
+}
+
+.personality-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: var(--bg-secondary);
+ border-radius: var(--radius);
+ margin-bottom: 8px;
+}
+
+.personality-icon {
+ margin-right: 4px;
+}
+
+.personality-badge {
+ font-size: 12px;
+ margin-left: 8px;
+}
+
+.personality-badge.preset {
+ color: var(--warning);
+}
+
+.personality-badge.custom {
+ color: var(--success);
+}
+
+/* 固定定位消息提示(配置页用) */
+.toast-message {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 1000;
+ min-width: 200px;
+}
diff --git a/assets/js/chat.js b/assets/js/chat.js
index 63a4ee5..5ffbfbb 100644
--- a/assets/js/chat.js
+++ b/assets/js/chat.js
@@ -306,3 +306,109 @@ const ChatManager = {
return div.innerHTML;
}
};
+
+// ========================================
+// 聊天页面初始化
+// ========================================
+
+// 切换侧边栏
+function toggleSidebar() {
+ document.getElementById('sidebar').classList.toggle('collapsed');
+}
+
+// 供应商/模型数据缓存
+let providersData = [];
+let personalitiesData = [];
+
+// 加载供应商配置
+async function loadProviders() {
+ try {
+ const res = await api.get('/config');
+ providersData = res.data.providers || [];
+ const select = document.getElementById('providerSelect');
+ select.innerHTML = '';
+ providersData.forEach((p, i) => {
+ if (p.enabled) {
+ const option = document.createElement('option');
+ option.value = i;
+ option.textContent = p.name;
+ select.appendChild(option);
+ }
+ });
+ // 如果只有一个供应商,自动选择
+ if (providersData.filter(p => p.enabled).length === 1) {
+ select.value = providersData.findIndex(p => p.enabled);
+ onProviderChange();
+ }
+ } catch (err) {
+ console.error('加载供应商配置失败:', err);
+ }
+}
+
+// 供应商切换时更新模型列表
+function onProviderChange() {
+ const index = document.getElementById('providerSelect').value;
+ const modelSelect = document.getElementById('modelSelect');
+ modelSelect.innerHTML = '';
+
+ if (index !== '' && providersData[index]) {
+ const provider = providersData[index];
+ (provider.models || []).forEach(m => {
+ const option = document.createElement('option');
+ option.value = m;
+ option.textContent = m;
+ modelSelect.appendChild(option);
+ });
+ // 如果只有一个模型,自动选择
+ if (provider.models && provider.models.length === 1) {
+ modelSelect.value = provider.models[0];
+ }
+ // 如果有默认模型,自动选择
+ if (provider.defaultModel) {
+ modelSelect.value = provider.defaultModel;
+ }
+ }
+}
+
+// 加载人格列表
+async function loadPersonalities() {
+ try {
+ const res = await api.get('/personalities');
+ personalitiesData = res.data || [];
+ const select = document.getElementById('personalitySelect');
+ select.innerHTML = '';
+ personalitiesData.forEach(p => {
+ const option = document.createElement('option');
+ option.value = p.id;
+ option.textContent = (p.icon ? p.icon + ' ' : '') + p.name;
+ select.appendChild(option);
+ });
+ } catch (err) {
+ console.error('加载人格列表失败:', err);
+ }
+}
+
+// 页面初始化
+(async function() {
+ // 检查认证
+ const token = Storage.getToken();
+ if (!token) {
+ window.location.href = '/login.php';
+ return;
+ }
+
+ // 初始化聊天管理器
+ ChatManager.init();
+
+ // 加载数据
+ await Promise.all([
+ loadProviders(),
+ loadPersonalities(),
+ SessionManager.loadSessions()
+ ]);
+
+ // 如果有会话,自动选择第一个
+ if (SessionManager.sessions.length > 0) {
+ await SessionManager.switchSession(SessionManager.sessions[0].id);
+ }
+})();
diff --git a/assets/js/config.js b/assets/js/config.js
new file mode 100644
index 0000000..84e7d35
--- /dev/null
+++ b/assets/js/config.js
@@ -0,0 +1,203 @@
+const ConfigPage = {
+ providers: [],
+ personalities: [],
+
+ async init() {
+ // 检查认证和管理员权限
+ const token = Storage.getToken();
+ if (!token) {
+ window.location.href = '/login.php';
+ return;
+ }
+
+ try {
+ // 验证管理员权限
+ const userRes = await api.get('/auth/me');
+ if (userRes.data.role !== 'admin') {
+ window.location.href = '/chat.php';
+ return;
+ }
+ } catch (err) {
+ window.location.href = '/login.php';
+ return;
+ }
+
+ await this.loadProviders();
+ await this.loadPersonalities();
+ },
+
+ async loadProviders() {
+ try {
+ const res = await api.get('/config');
+ this.providers = res.data.providers || [];
+ this.renderProviders();
+ } catch (err) {
+ this.showMessage('加载供应商配置失败: ' + err.message, 'error');
+ }
+ },
+
+ renderProviders() {
+ const list = document.getElementById('providerList');
+ if (this.providers.length === 0) {
+ list.innerHTML = '暂无供应商配置
';
+ return;
+ }
+
+ list.innerHTML = this.providers.map((p, i) => `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `).join('');
+ },
+
+ addProvider() {
+ this.providers.push({
+ name: '新供应商',
+ apiUrl: '',
+ apiKey: '',
+ models: [],
+ type: 'newapi',
+ enabled: true
+ });
+ this.renderProviders();
+ this.saveProviders();
+ },
+
+ updateProvider(index, field, value) {
+ this.providers[index][field] = value;
+ this.saveProviders();
+ },
+
+ updateProviderModels(index, value) {
+ this.providers[index].models = value.split(',').map(m => m.trim()).filter(m => m);
+ this.saveProviders();
+ },
+
+ toggleProvider(index, enabled) {
+ this.providers[index].enabled = enabled;
+ this.saveProviders();
+ },
+
+ deleteProvider(index) {
+ if (!confirm('确定要删除供应商 "' + this.providers[index].name + '" 吗?')) return;
+ this.providers.splice(index, 1);
+ this.renderProviders();
+ this.saveProviders();
+ },
+
+ async saveProviders() {
+ try {
+ await api.put('/config', { providers: this.providers });
+ this.showMessage('供应商配置已保存', 'success');
+ } catch (err) {
+ this.showMessage('保存失败: ' + err.message, 'error');
+ }
+ },
+
+ async loadPersonalities() {
+ try {
+ const res = await api.get('/personalities');
+ this.personalities = res.data || [];
+ this.renderPersonalities();
+ } catch (err) {
+ this.showMessage('加载人格列表失败: ' + err.message, 'error');
+ }
+ },
+
+ renderPersonalities() {
+ const list = document.getElementById('personalityList');
+ if (this.personalities.length === 0) {
+ list.innerHTML = '暂无人格
';
+ return;
+ }
+
+ list.innerHTML = this.personalities.map(p => `
+
+
+ ${p.icon ? '' + this.escapeHtml(p.icon) + '' : ''}${this.escapeHtml(p.name)}
+ ${p.is_preset ? '预设' : '自定义'}
+
+ ${!p.is_preset ? '
' : ''}
+
+ `).join('');
+ },
+
+ async createPersonality() {
+ const name = document.getElementById('newPersonalityName').value.trim();
+ const prompt = document.getElementById('newPersonalityPrompt').value.trim();
+ const icon = document.getElementById('newPersonalityIcon').value.trim();
+ const description = document.getElementById('newPersonalityDesc').value.trim();
+
+ if (!name || !prompt) {
+ this.showMessage('名称和提示词不能为空', 'error');
+ return;
+ }
+
+ try {
+ await api.post('/personalities', { name, prompt, icon, description });
+ this.showMessage('人格创建成功', 'success');
+ // 清空表单
+ document.getElementById('newPersonalityName').value = '';
+ document.getElementById('newPersonalityPrompt').value = '';
+ document.getElementById('newPersonalityIcon').value = '';
+ document.getElementById('newPersonalityDesc').value = '';
+ await this.loadPersonalities();
+ } catch (err) {
+ this.showMessage('创建失败: ' + err.message, 'error');
+ }
+ },
+
+ async deletePersonality(id) {
+ if (!confirm('确定要删除这个人格吗?')) return;
+ try {
+ await api.delete('/personalities/' + id);
+ this.showMessage('人格已删除', 'success');
+ await this.loadPersonalities();
+ } catch (err) {
+ this.showMessage('删除失败: ' + err.message, 'error');
+ }
+ },
+
+ showMessage(text, type) {
+ const el = document.getElementById('configMessage');
+ el.innerHTML = `${this.escapeHtml(text)}
`;
+ setTimeout(() => { el.innerHTML = ''; }, 3000);
+ },
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+};
+
+ConfigPage.init();
diff --git a/assets/js/install.js b/assets/js/install.js
new file mode 100644
index 0000000..167a72d
--- /dev/null
+++ b/assets/js/install.js
@@ -0,0 +1,201 @@
+const InstallWizard = {
+ currentStep: 1,
+ totalSteps: 5,
+
+ init() {
+ this.checkEnv();
+ this.addProvider(); // 默认添加一个供应商表单
+ },
+
+ checkEnv() {
+ const checks = document.getElementById('envCheckData');
+ if (!checks) return;
+
+ const data = JSON.parse(checks.textContent);
+
+ const container = document.getElementById('envChecks');
+ container.innerHTML = data.map(c => `
+
+
${c.name}
+
+ ${c.pass
+ ? ' 通过'
+ : ' 未通过'
+ }
+
+
+ `).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 = `${data.message}
`;
+ } catch (err) {
+ result.innerHTML = '连接失败: ' + err.message + '
';
+ }
+ },
+
+ addProvider() {
+ const list = document.getElementById('providerList');
+ const index = list.children.length;
+ const html = `
+
+ `;
+ 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 = '安装成功!正在跳转到登录页...
';
+ setTimeout(() => { window.location.href = '/login.php'; }, 2000);
+ } else {
+ result.innerHTML = '安装失败: ' + data.message + '
';
+ btn.disabled = false;
+ btn.textContent = '完成安装';
+ }
+ } catch (err) {
+ result.innerHTML = '安装失败: ' + err.message + '
';
+ btn.disabled = false;
+ btn.textContent = '完成安装';
+ }
+ }
+};
+
+InstallWizard.init();
diff --git a/assets/js/login.js b/assets/js/login.js
new file mode 100644
index 0000000..f40cd2e
--- /dev/null
+++ b/assets/js/login.js
@@ -0,0 +1,50 @@
+// 页面加载时检查是否已登录
+(function() {
+ const token = localStorage.getItem('token');
+ if (token) {
+ window.location.href = '/chat.php';
+ }
+})();
+
+// 登录表单提交
+document.getElementById('loginForm').addEventListener('submit', async function(e) {
+ e.preventDefault();
+ const btn = document.getElementById('loginBtn');
+ const errorEl = document.getElementById('loginError');
+ const username = document.getElementById('username').value.trim();
+ const password = document.getElementById('password').value;
+
+ if (!username || !password) {
+ errorEl.textContent = '请输入用户名和密码';
+ errorEl.classList.remove('hidden');
+ return;
+ }
+
+ btn.disabled = true;
+ btn.textContent = '登录中...';
+ errorEl.classList.add('hidden');
+
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ localStorage.setItem('token', data.data.token);
+ window.location.href = '/chat.php';
+ } else {
+ errorEl.textContent = data.message || '登录失败';
+ errorEl.classList.remove('hidden');
+ }
+ } catch (err) {
+ errorEl.textContent = '网络错误,请稍后重试';
+ errorEl.classList.remove('hidden');
+ } finally {
+ btn.disabled = false;
+ btn.textContent = '登录';
+ }
+});
diff --git a/public/install.php b/public/install.php
index 6dfad90..a5955da 100644
--- a/public/install.php
+++ b/public/install.php
@@ -5,6 +5,15 @@ if (file_exists($configFile)) {
header('Location: /login.php');
exit;
}
+
+// 环境检查数据(JSON 格式供 JS 使用)
+$envChecks = json_encode([
+ ['name' => 'PHP 版本 >= 8.0', 'pass' => version_compare(PHP_VERSION, '8.0.0', '>=')],
+ ['name' => 'PDO 扩展', 'pass' => extension_loaded('pdo')],
+ ['name' => 'cURL 扩展', 'pass' => extension_loaded('curl')],
+ ['name' => 'uploads/ 目录可写', 'pass' => is_writable(__DIR__ . '/../uploads')],
+ ['name' => 'config/ 目录可写', 'pass' => is_writable(__DIR__ . '/../config')],
+]);
?>
@@ -15,6 +24,9 @@ if (file_exists($configFile)) {
+
+
+