RAM 角色自动探测 + 邮件配置界面 + 每个软件独立主题色 (#1 #2)

## 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)
This commit is contained in:
2026-11-01 16:39:09 +08:00
parent c19e475e85
commit f1a81cbaa1
12 changed files with 498 additions and 12 deletions
+45
View File
@@ -136,9 +136,54 @@ function fb_oss_metadata_token() {
return trim((string)$body);
}
/**
* 自动探测本 ECS 实例绑定的 RAM 角色名。
*
* 元数据接口 GET /latest/meta-data/ram/security-credentials/(末尾带斜杠)
* 返回的是「角色名列表」,每行一个。绝大多数实例只绑一个角色。
* 所以用户完全不需要知道角色叫什么 —— 我们去问机器就行。
*
* @return array [bool $ok, string $role|string $err]
*/
function fb_oss_detect_instance_role() {
$url = fb_oss_metadata_base() . '/latest/meta-data/ram/security-credentials/';
$headers = [];
$tok = fb_oss_metadata_token();
if ($tok !== '') { $headers[] = 'X-aliyun-ECS-Metadata-Token: ' . $tok; }
list($st, , $body) = fb_http('GET', $url, [
'timeout' => 3, 'noproxy' => true, 'headers' => $headers,
]);
if ($st < 200 || $st >= 300) {
return [false, '读不到 ECS 元数据(HTTP ' . $st . ')。不在 ECS 上,或元数据服务被禁用。'];
}
$txt = trim((string)$body);
if ($txt === '') {
return [false, '这台 ECS 实例没有绑定任何 RAM 角色。请在阿里云控制台给实例绑定一个角色,再回来点「自动探测」。'];
}
// 可能是多行(多角色)或 JSON 数组,两种都兼容
$names = [];
$j = json_decode($txt, true);
if (is_array($j)) {
foreach ($j as $v) { if (is_string($v) && trim($v) !== '') { $names[] = trim($v); } }
} else {
foreach (preg_split('/\r?\n/', $txt) as $line) {
$line = trim($line);
if ($line !== '') { $names[] = $line; }
}
}
if (!$names) { return [false, '元数据返回内容无法识别为角色名。']; }
return [true, $names[0]]; // 多角色时取第一个,后台会提示可手改
}
/** 从 ECS 元数据读取实例 RAM 角色凭证 @return array [bool, array|string] */
function fb_oss_instance_creds() {
$role = trim((string)fb_oss_cfg('instance_role', ''));
// 没填就自动探测一次,免得用户还要去控制台查角色名
if ($role === '') {
list($ok, $r) = fb_oss_detect_instance_role();
if ($ok) { $role = (string)$r; }
}
if ($role === '') { return [false, '未配置 oss.instance_role']; }
$url = fb_oss_metadata_base() . '/latest/meta-data/ram/security-credentials/' . rawurlencode($role);