From f1a81cbaa165d15372cba69b47cfcd9755885caa Mon Sep 17 00:00:00 2001 From: Yongquan Date: Sun, 1 Nov 2026 16:39:09 +0800 Subject: [PATCH] =?UTF-8?q?RAM=20=E8=A7=92=E8=89=B2=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B=20+=20=E9=82=AE=E4=BB=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=20+=20=E6=AF=8F=E4=B8=AA=E8=BD=AF=E4=BB=B6?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E4=B8=BB=E9=A2=98=E8=89=B2=20(#1=20#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 1. ECS RAM 角色改为自动探测 原先要用户去阿里云控制台查「实例 RAM 角色名」再手填,纯属多余 —— 机器自己知道答案。 新增 fb_oss_detect_instance_role():读元数据 GET /latest/meta-data/ram/security-credentials/(末尾带斜杠)拿到角色名列表。 - 兼容单行 / 多行 / JSON 数组三种返回 - 未配置角色时 fb_oss_instance_creds() 自动探测,不再报「未配置」 - 后台改成只读展示「已绑定:xxx」,手填入口收进「高级」折叠里 - 保存时留空 = 自动探测后写入,不存空值 - 探测失败给出可操作原因(没绑角色 / 不在 ECS 上 / 元数据被禁用) ## 2. 邮件配置界面(原先完全缺失) lib/mailer.php 有完整 SMTP 实现,但后台一个输入框都没有, 只能改 config.php —— 用户装完根本找不到地方配。 根因有两层: - fb_setting_keys() 白名单里没有任何 smtp.* / notify.* 键, 就算加了表单,fb_set_setting() 也会把值全部丢弃 - 设置页只有「站点信息」「附件存储」两个区块 补上:白名单 15 个键 + 后台「邮件通知」区块 - SMTP 服务器/端口/加密方式(SSL/STARTTLS/不加密) - 登录账号/密码(留空不修改,避免每次重填) - 发件地址/显示名、超时、SSL 校验 - 通知规则:回复通知、每次回复都通知、新反馈通知管理员、发信冷却 - 「发送测试邮件」按钮,AJAX 发完就地显示成功或失败原因, 不用保存再靠猜 ## 3. 每个软件一个主题色 需求:从短链接进不同软件,颜色不同,一眼区分。 - software 表加 color 字段(幂等迁移) - 全站配色靠 --brand / --brand-600 / --brand-soft 三个变量, 按软件覆写这三个即可,不用为每个软件写一套 CSS - fb_color_variants() 由主色推出深一档(×0.85)与浅一档(往白混 88%) - 未配色的软件按 id 从 8 色调色板取默认色,保证同站不撞色 - 后台:颜色选择器(原生取色 + 8 个预设 + 用默认) - 前台:index / new / thread 都按当前软件注入主题色, meta theme-color 也跟着变 验证:三功能专项 30/30;新旧回归全过 (全新安装 33/33、老基线 25/25、附件 22/22、安装向导 60/60、约束检查 20/20) --- admin.php | 191 ++++++++++++++++++++++++++++++++++++++++- assets/app.js | 89 +++++++++++++++++++ assets/style.css | 44 ++++++++++ index.php | 2 +- lib/admin_taxonomy.php | 30 ++++++- lib/bootstrap.php | 2 +- lib/db.php | 8 ++ lib/layout.php | 21 ++++- lib/oss.php | 45 ++++++++++ lib/util.php | 73 ++++++++++++++++ new.php | 2 +- thread.php | 3 +- 12 files changed, 498 insertions(+), 12 deletions(-) diff --git a/admin.php b/admin.php index 4bd206e..a254b09 100644 --- a/admin.php +++ b/admin.php @@ -108,7 +108,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) { $t = fb_admin_thread($pdo, $id); // 这些动作不针对某一条反馈(不依赖 $t),单独放行 - $noThread = in_array($act, ['settings', 'password'], true) + $noThread = in_array($act, ['settings', 'password', 'mail_test'], true) || fb_admin_is_taxonomy_act($act); if (!$t && !$noThread) { @@ -200,6 +200,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) { fb_mod_log('delete', $id, '删除问题:' . fb_excerpt((string)$t['title'], 40)); $msg = '#' . $id . ' 已删除。'; + } elseif ($act === 'mail_test') { + // 发送测试邮件:只验证 SMTP 通不通,不改任何配置 + $to = trim((string)(isset($_POST['to']) ? $_POST['to'] : '')); + if ($to === '') { $to = trim((string)fb_cfg('admin_email', '')); } + if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { + $msg = '请填写有效的收件邮箱。'; + $msgType = 'error'; + } else { + $sc = (array)fb_cfg('smtp', []); + $site = (string)fb_cfg('site_name', '轻反馈'); + $body = '

这是一封来自 ' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') + . ' 的测试邮件。

' + . '

如果你收到了它,说明 SMTP 配置正确,回复通知可以正常发出。

' + . '

发送时间:' + . date('Y-m-d H:i:s') . '

'; + list($ok, $err) = fb_smtp_send($sc, $to, '【' . $site . '】邮件测试', $body); + if ($ok) { + $msg = '测试邮件已发出,请查收 ' . $to . '(也看一下垃圾箱)。'; + $msgType = 'ok'; + } else { + $msg = '发送失败:' . $err; + $msgType = 'error'; + } + } + } elseif (fb_admin_is_taxonomy_act($act)) { list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST); @@ -297,6 +322,12 @@ function fb_admin_save_settings() { $v = trim((string)$post($k, '')); // 对象键前缀留空会让对象键变成 "/2026/...",回落到出厂默认 'fb' if ($k === 'oss.prefix' && $v === '') { fb_unset_setting($k); continue; } + // RAM 角色留空 = 让程序自己去元数据服务探测,不写死一个空值 + if ($k === 'oss.instance_role' && $v === '') { + list($rOk, $rVal) = fb_oss_detect_instance_role(); + if ($rOk) { fb_set_setting($k, (string)$rVal); } + continue; // 探测不到就保持原值不动 + } fb_set_setting($k, $v); } @@ -311,6 +342,48 @@ function fb_admin_save_settings() { fb_set_setting('oss.verify', $post('oss.verify') ? 1 : 0); fb_set_setting('oss.image_process', $post('oss.image_process') ? 1 : 0); fb_set_setting('oss.use_internal_endpoint', $post('oss.use_internal_endpoint') ? 1 : 0); + /* ---------------- 邮件通知(SMTP) ---------------- */ + $smtpText = ['smtp.host', 'smtp.user', 'smtp.from', 'smtp.from_name']; + foreach ($smtpText as $k) { + fb_set_setting($k, trim((string)$post($k, ''))); + } + // 口令留空 = 不修改(避免每次保存都得重填) + $sp = (string)(isset($_POST['smtp_pass']) ? $_POST['smtp_pass'] : ''); + if (trim($sp) !== '') { fb_set_setting('smtp.pass', $sp); } + + $smtpPort = (int)$post('smtp.port', 465); + if ($smtpPort <= 0 || $smtpPort > 65535) { $smtpPort = 465; } + fb_set_setting('smtp.port', $smtpPort); + + $sec = strtolower(trim((string)$post('smtp.secure', 'ssl'))); + if (!in_array($sec, ['ssl', 'tls', ''], true)) { $sec = 'ssl'; } + fb_set_setting('smtp.secure', $sec); + + $smtpTo = (int)$post('smtp.timeout', 15); + fb_set_setting('smtp.timeout', max(5, min(60, $smtpTo))); + fb_set_setting('smtp.verify_ssl', (int)(bool)$post('smtp.verify_ssl')); + + // 没填 from 时用 user 兜底(绝大多数 SMTP 要求两者一致) + if (trim((string)fb_cfg('smtp.from', '')) === '') { + fb_set_setting('smtp.from', trim((string)fb_cfg('smtp.user', ''))); + } + + // 只有 host 填了才允许开启,否则开着一个空配置只会每次发信都失败 + $wantOn = (bool)$post('smtp.enabled'); + $hostNow = trim((string)fb_cfg('smtp.host', '')); + fb_set_setting('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0); + if ($wantOn && $hostNow === '') { + $msg = '邮件通知未开启:请先填写 SMTP 服务器地址。'; + $msgType = 'error'; + } + + /* ---------------- 通知开关 ---------------- */ + fb_set_setting('notify.enabled', (int)(bool)$post('notify.enabled')); + fb_set_setting('notify.on_every_reply', (int)(bool)$post('notify.on_every_reply')); + fb_set_setting('notify.admin_new_thread', (int)(bool)$post('notify.admin_new_thread')); + $cd = (int)$post('notify.cooldown_seconds', 60); + fb_set_setting('notify.cooldown_seconds', max(0, min(86400, $cd))); + fb_set_setting('moderation.enabled', $post('moderation.enabled') ? 1 : 0); fb_mod_log('settings', null, '保存站点设置:存储=' . $storage); @@ -654,6 +727,88 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']); value=""> 页脚「FeedLite」二字指向的地址,默认是本项目开源仓库。只接受 http:// 或 https:// 开头的网址;填回默认值即恢复。 +

邮件通知

+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ 高级选项 +
+ + +
+
+ + +
+ 配好后先发一封测试邮件确认能通: + + + +
+ + +

通知规则

+ + + + +

附件存储

- + +
+ ECS 实例 RAM 角色 + +
已绑定: + (自动从实例元数据读取,无需手填) +
+ +
自动探测到: + 保存时若留空就自动用这个 +
+ +
+ +
+ +
+ 手动指定角色名(一般不需要) + +
+