Преглед изворни кода

feat: 新增可视化配置编辑器

sansan пре 3 месеци
родитељ
комит
bd1988c59b
12 измењених фајлова са 4597 додато и 52 уклоњено
  1. 47 20
      README-EN.md
  2. 58 24
      README.md
  3. 4 1
      config/config.yaml
  4. 5 2
      config/frequency_words.txt
  5. 3500 0
      docs/assets/script.js
  6. 560 0
      docs/assets/style.css
  7. BIN
      docs/assets/weixin.webp
  8. 418 0
      docs/index.html
  9. 1 1
      pyproject.toml
  10. 1 1
      trendradar/__init__.py
  11. 1 1
      version
  12. 2 2
      version_configs

+ 47 - 20
README-EN.md

@@ -8,12 +8,10 @@ Deploy in <strong>30 seconds</strong> — Say goodbye to endless scrolling, only
 
 <a href="https://trendshift.io/repositories/14726" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14726" alt="sansan0%2FTrendRadar | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 
-<a href="https://shandianshuo.cn" target="_blank" title="AI Voice Input, 4x Faster Than Typing ⚡"><img src="_image/shandianshuo.png" alt="FlashSpeak logo" height="50"/></a>
-
 [![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
 [![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v5.4.0-blue.svg)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v5.5.0-blue.svg)](https://github.com/sansan0/TrendRadar)
 [![MCP](https://img.shields.io/badge/MCP-v3.1.7-green.svg)](https://github.com/sansan0/TrendRadar)
 [![RSS](https://img.shields.io/badge/RSS-Feed_Support-orange.svg?style=flat-square&logo=rss&logoColor=white)](https://github.com/sansan0/TrendRadar)
 [![AI Translation](https://img.shields.io/badge/AI-Multi--Language-purple.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
@@ -137,34 +135,63 @@ After communication, the author indicated no concerns about server pressure, but
 
 ## 🪄 Sponsors
 
-> Writing reports, replying messages making your wrists tired? Try「FlashSpeak」AI Voice Input - Speak instead of type, 4x faster ⚡
-
 <div align="center">
 
-[![Mac Download](https://img.shields.io/badge/Mac-Free_Download-FF6B6B?style=for-the-badge&logo=apple&logoColor=white)](https://shandianshuo.cn) [![Windows Download](https://img.shields.io/badge/Windows-Free_Download-FF6B6B?style=for-the-badge&logo=lightning&logoColor=white)](https://shandianshuo.cn)
-<a href="https://shandianshuo.cn" target="_blank">
-  <img src="_image/banner-shandianshuo.png" alt="FlashSpeak" width="600"/>
-</a>
+> **Sponsorship Open**  
+>
+> Seeking quality product partners.  
+> More than features, I value **your attitude towards users**.  
+> Please share your user community or feedback channels—I want to see how you support your users.  
+> [📩 Contact Me](mailto:path@linux.do)  
+
 </div>
 
 <br>
 
-## ☕ Support Project
+<a name="-support-project"></a>
 
-> If this project is helpful to you, you can choose the following ways to support:
-> 1. **Public Welfare**: Search for **Tencent Charity** on WeChat and donate to **Education Support** related projects as you wish.
->
-> 2. **Sponsor the Developer**: Your sponsorship will be used to replenish caffeine for the carbon-based organism and Token consumption for the silicon-based organism.
+## ✨ Find it useful? Support TrendRadar
 
+TrendRadar is a completely free and open-source project. Your support fuels the motivation for continuous updates.
 
-- **GitHub Issues**: Suitable for targeted answers. Please provide complete info when asking (screenshots, error logs, system environment, etc.).
-- **Official Account**: Suggested for interaction. Please prioritize public comments under relevant articles. If you need to ask questions, liking, recommending, or sharing articles to show support is highly appreciated! (´▽`ʃ♡ƪ).
-  <br>*(Friendly Reminder: This is a free open-source project, not a commercial service. Please check the documentation first if you encounter issues. Patience and courtesy are expected. I cannot respond to demands for customer support or emotional accusations. Thank you for understanding. Additionally, significant effort went into the documentation; it is strongly recommended to read the [**🚀 Quick Start**](#-quick-start) section first, where most deployment answers can be found.)*
+### ❤️ Donate
 
+A bottle of water or a snack represents your love.
+Any amount is welcome; even 1 RMB is a gesture of kindness.
 
-| Official Account | WeChat Appreciation | Alipay Appreciation |
-|:---:|:---:|:---:|
-| <img src="_image/weixin.png" width="300" title="Silicon Tea Room"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="300" title="WeChat Pay"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="300" title="Alipay"/> |
+> Your sponsorship will be used to replenish caffeine for carbon-based lifeforms ☕️ and API Tokens for silicon-based lifeforms 🤖.
+
+<div align="center">
+
+| WeChat Pay | Alipay |
+| --- | --- |
+| <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="240" alt="WeChat Pay"> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="240" alt="Alipay"> |
+
+</div>
+
+### 🌟 Other Ways to Support
+
+1. **Star the Repo** ⭐️: It only takes **1 second**. Letting more people see this project is the greatest recognition for me.
+2. **Charity** 🌻: Search for **Tencent Charity** (or support a local charity) to help students in need. Pass this kindness forward.
+
+---
+
+### 💬 Feedback & Community
+
+* **GitHub Issues**: Best for specific technical issues. Please provide complete information (screenshots, error logs, etc.) to help locate the problem quickly.
+* **WeChat Official Account**: It is recommended to leave comments under relevant articles. If you need to ask questions in the background, **liking/recommending** the article first is the best "icebreaker," and I can feel your appreciation (´▽`ʃ♡ƪ).
+
+> **Friendly Reminder**:  
+> This project is for open-source sharing, not a commercial product. A lot of effort went into the documentation; most deployment issues can be answered in **[🚀 Quick Start](#-quick-start)**.   
+> *Please be patient and polite when asking questions. Treat the author as a friend, not customer service, for better communication efficiency!*  
+
+<div align="center">
+
+| Follow on WeChat |
+| --- |
+| <img src="_image/weixin.png" width="500" title="Silicon-based Tea Room"/> |
+
+</div>
 
 <br>
 

+ 58 - 24
README.md

@@ -8,12 +8,11 @@
 
 <a href="https://trendshift.io/repositories/14726" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14726" alt="sansan0%2FTrendRadar | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 
-<a href="https://shandianshuo.cn" target="_blank" title="AI 语音输入,比打字快 4 倍 ⚡"><img src="_image/shandianshuo.png" alt="闪电说 logo" height="50"/></a>
 
 [![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
 [![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
 [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
-[![Version](https://img.shields.io/badge/version-v5.4.0-blue.svg)](https://github.com/sansan0/TrendRadar)
+[![Version](https://img.shields.io/badge/version-v5.5.0-blue.svg)](https://github.com/sansan0/TrendRadar)
 [![MCP](https://img.shields.io/badge/MCP-v3.1.7-green.svg)](https://github.com/sansan0/TrendRadar)
 [![RSS](https://img.shields.io/badge/RSS-订阅源支持-orange.svg?style=flat-square&logo=rss&logoColor=white)](https://github.com/sansan0/TrendRadar)
 [![AI翻译](https://img.shields.io/badge/AI-多语言推送-purple.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
@@ -184,34 +183,62 @@
 
 ## 🪄 赞助商
 
-> 每天写报告、回复消息是否让手腕疲惫?试试「闪电说」AI 语音输入法 —— 说话,比打字快 4 倍 ⚡ 
-
 <div align="center">
 
-[![Mac下载](https://img.shields.io/badge/Mac-免费下载-FF6B6B?style=for-the-badge&logo=apple&logoColor=white)](https://shandianshuo.cn) [![Windows下载](https://img.shields.io/badge/Windows-免费下载-FF6B6B?style=for-the-badge&logo=lightning&logoColor=white)](https://shandianshuo.cn)
-<a href="https://shandianshuo.cn" target="_blank">
-  <img src="_image/banner-shandianshuo.png" alt="闪电说" width="600"/>
-</a>
+> **虚位以待**
+>
+> 寻找靠谱的产品赞助。   
+> 比起功能,我更看重**你对待用户的态度**。  
+> 请附带你的用户群或反馈渠道,让我看到你是如何帮助用户解决问题的。  
+> [📩 点击联系](mailto:path@linux.do) 
+
 </div>
 
 <br>
 
-## ☕ 支持项目
+<a name="-支持项目"></a>
+## ✨ 觉得好用?支持一下
 
-> 如果本项目对你有帮助,你可以选择以下方式支持:
-> 1. **公益助学**:微信搜索**腾讯公益**,对里面的**助学**相关的项目随心捐。
->
-> 2. **赞助开发者**:你的赞助将用于补充碳基生物的咖啡因和硅基生物的 Token 消耗。
+TrendRadar 是完全开源免费的项目,持续更新需要你的动力支持。
 
+### ❤️ 随心赞赏
 
-- **GitHub Issues**:适合针对性强的解答。提问时请提供完整信息(截图、错误日志、系统环境等)。
-- **公众号交流**:建议优先在相关文章下的公共留言区交流。若需提问,欢迎先点赞、推荐或分享文章表达支持,我在后台都能感受到这份心意哟 (´▽`ʃ♡ƪ)。
-  <br>*(友情提示:本项目为免费开源分享,非商业服务。遇到部署问题请先查阅文档,提问请保持耐心与礼貌。对于将开源作者视为客服或带有情绪的指责,恕难回应,感谢理解。此外,文档倾注了大量心血,强烈建议优先阅读 [**🚀 快速开始**](#-快速开始) 章节,绝大多数部署问题都能从中找到答案。)*
+一瓶水、一包辣条都是爱。
+金额随意,1 元也是一份心意。
 
+> 你的赞助将用于补充碳基生物的咖啡因 ☕️ 和硅基生物的 API Token 消耗 🤖。
 
-|公众号关注 |微信点赞 | 支付宝点赞 |
-|:---:|:---:|:---:|
-| <img src="_image/weixin.png" width="300" title="硅基茶水间"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="300" title="微信支付"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="300" title="支付宝支付"/> |
+<div align="center">
+
+| 微信赞赏 | 支付宝赞赏 |
+|:---:|:---:|
+| <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="240" alt="微信赞赏"> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="240" alt="支付宝赞赏"> |
+
+</div>
+
+### 🌟 其他支持方式
+
+1. **点亮 Star** ⭐️:动动手指只需 **1 秒**,让更多人看到这个项目,这是对我最大的认可。
+2. **公益助学** 🌻:微信搜索**腾讯公益**,支持**助学**项目。将这份善意传递给需要的人。
+
+---
+
+### 💬 交流与反馈
+
+- **GitHub Issues**:适合具体的技术问题。提问时请提供完整信息(截图、错误日志等),有助于快速定位。
+- **公众号交流**:建议优先在相关文章下的留言区交流。若需后台提问,**先点赞/推荐**文章是最好的“敲门砖”,我在后台都能感受到这份心意哟 (´▽`ʃ♡ƪ)。
+
+> **友情提示**:   
+> 本项目为开源分享,非商业产品。文档倾注了大量心血,绝大多数部署问题都能在 [**🚀 快速开始**](#-快速开始) 中找到答案。   
+> *提问请保持耐心与礼貌,把作者当朋友而非客服,沟通效率会更高哦!*  
+
+<div align="center">
+
+|公众号关注 |
+|:---:|
+| <img src="_image/weixin.png" width="500" title="硅基茶水间"/> |
+
+</div>
 
 <br>
 
@@ -221,12 +248,11 @@
 - **提示**:建议查看【历史更新】,明确具体的【功能内容】
 
 
-### 2026/01/23 - v5.4.0
+### 2026/01/28 - v5.5.0
 
-- 增加 AI 分析模式的独立控制功能,可选 follow_report | daily | current | incremental 
-- 新增 AI 分析时间窗口控制,支持自定义运行段及每日频次限制
-- 增加配置文件版本管理功能
-- 修复若干bug
+> 和 mcp 功能一样, 这个小工具我也不新开一个仓库维护了, 反正纯前端, 都搁一起吧
+
+- 增加 trendradar 的可视化配置编辑器 
 
 
 ### 2026/01/10 - mcp-v3.0.0~v3.1.5
@@ -244,6 +270,14 @@
 <summary>👉 点击展开:<strong>历史更新</strong></summary>
 
 
+### 2026/01/23 - v5.4.0
+
+- 增加 AI 分析模式的独立控制功能,可选 follow_report | daily | current | incremental 
+- 新增 AI 分析时间窗口控制,支持自定义运行段及每日频次限制
+- 增加配置文件版本管理功能
+- 修复若干bug
+
+
 ### 2026/01/19 - v5.3.0
 
 > **重大重构:AI 模块迁移至 LiteLLM**

+ 4 - 1
config/config.yaml

@@ -1,9 +1,12 @@
 # ═══════════════════════════════════════════════════════════════
 #                    TrendRadar 配置文件
-#                      Version: 1.0.0
+#                      Version: 1.1.0
 # ═══════════════════════════════════════════════════════════════
 
 
+# 可视化配置编辑器地址: https://sansan0.github.io/TrendRadar/
+
+
 # ===============================================================
 # 1. 基础设置
 # ===============================================================

+ 5 - 2
config/frequency_words.txt

@@ -1,7 +1,10 @@
 # ═══════════════════════════════════════════════════════════════
 #                    TrendRadar 频率词配置文件
-#                         Version: 1.0.0
+#                         Version: 1.1.0
 # ═══════════════════════════════════════════════════════════════
+
+# 可视化配置编辑器地址: https://sansan0.github.io/TrendRadar/
+#
 # 凡是左侧有 # 的都是仅供阅读的说明性文字
 #
 # 这个文件用来设置你想关注的新闻关键词。
@@ -225,7 +228,7 @@
 # ═══════════════════════════════════════════════════════════════
 
 [AI 相关]
-/(?<![a-zA-Z])ai(?![a-zA-Z])/i
+/(?<![a-zA-Z])ai(?![a-zA-Z])/
 人工智能
 
 [芯片]

+ 3500 - 0
docs/assets/script.js

@@ -0,0 +1,3500 @@
+/**
+ * TrendRadar 配置文件编辑器核心逻辑
+ * 特点:确保原始 YAML 的注释和格式 100% 保留
+ */
+
+// ==========================================
+// 0. 注释高亮功能
+// ==========================================
+
+/**
+ * 对文本应用高亮,# 后的内容显示为灰色
+ */
+function applyHighlight(text) {
+    const escape = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+    return text.split('\n').map(line => {
+        const idx = line.indexOf('#');
+        if (idx === -1) return escape(line);
+        return escape(line.slice(0, idx)) + '<span class="syntax-comment">' + escape(line.slice(idx)) + '</span>';
+    }).join('\n');
+}
+
+/**
+ * 更新高亮层
+ */
+function updateBackdrop(textareaId, backdropId) {
+    const ta = document.getElementById(textareaId);
+    const bd = document.getElementById(backdropId);
+    if (ta && bd) bd.innerHTML = applyHighlight(ta.value) + '\n';
+}
+
+/**
+ * 同步滚动
+ */
+function syncScroll(textareaId, backdropId) {
+    const ta = document.getElementById(textareaId);
+    const bd = document.getElementById(backdropId);
+    if (ta && bd) {
+        bd.scrollTop = ta.scrollTop;
+        bd.scrollLeft = ta.scrollLeft;
+    }
+}
+
+// ==========================================
+// 12. 支持项目弹窗逻辑
+// ==========================================
+
+/**
+ * 打开支持弹窗
+ */
+function openSupportModal() {
+    const modal = document.getElementById('support-modal');
+    if (modal) {
+        modal.classList.remove('hidden');
+        document.body.style.overflow = 'hidden'; // 禁止背景滚动
+    }
+}
+
+/**
+ * 关闭支持弹窗
+ */
+function closeSupportModal() {
+    const modal = document.getElementById('support-modal');
+    if (modal) {
+        modal.classList.add('hidden');
+        document.body.style.overflow = ''; // 恢复滚动
+    }
+}
+
+/**
+ * 点击外部关闭
+ */
+function closeSupportModalOutside(event) {
+    if (event.target.id === 'support-modal') {
+        closeSupportModal();
+    }
+}
+
+window.openSupportModal = openSupportModal;
+window.closeSupportModal = closeSupportModal;
+window.closeSupportModalOutside = closeSupportModalOutside;
+const MODULE_DEFS = [
+    { id: 1, name: "1. 基础设置", key: "app", editable: false },
+    { id: 2, name: "2. 数据源 - 热榜平台", key: "platforms", editable: true },
+    { id: 3, name: "3. 数据源 - RSS 订阅", key: "rss", editable: true },
+    { id: 4, name: "4. 报告模式", key: "report", editable: true },
+    { id: 5, name: "5. 推送内容控制", key: "display", editable: true },
+    { id: 6, name: "6. 推送通知 (仅限时间窗口)", key: "notification", editable: true, partial: true },
+    { id: 7, name: "7. 存储配置", key: "storage", editable: false },
+    { id: 8, name: "8. AI 模型配置", key: "ai", editable: true },
+    { id: 9, name: "9. AI 分析功能", key: "ai_analysis", editable: true },
+    { id: 10, name: "10. AI 翻译功能", key: "ai_translation", editable: true },
+    { id: 11, name: "11. 高级设置", key: "advanced", editable: false }
+];
+
+// 初始默认内容 (用于空状态) - 只显示提示文本
+const INITIAL_YAML = `# 在此粘贴你的 config.yaml...
+# 或拖拽文件到编辑器区域
+# 或点击右上角"加载官网最新配置"`;
+
+// LocalStorage 键名
+const STORAGE_KEY_CONFIG = 'trendradar_config_yaml';
+const STORAGE_KEY_FREQUENCY = 'trendradar_frequency_txt';
+const STORAGE_KEY_CONFIG_TIME = 'trendradar_config_time';
+const STORAGE_KEY_FREQUENCY_TIME = 'trendradar_frequency_time';
+
+// 官网配置文件 URL
+const REMOTE_CONFIG_URL = 'https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/config/config.yaml';
+const REMOTE_FREQUENCY_URL = 'https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/config/frequency_words.txt';
+const REMOTE_VERSION_URL = 'https://raw.githubusercontent.com/sansan0/TrendRadar/refs/heads/master/version_configs';
+
+let currentYaml = "";
+let currentFrequency = "";
+let currentFrequencyData = null;  // 缓存解析后的数据,避免重复解析导致索引错位
+let currentTab = "config";
+
+// ==========================================
+// 2. 初始化与事件绑定
+// ==========================================
+// 防抖定时器
+let configSaveTimer = null;
+let frequencySaveTimer = null;
+
+document.addEventListener('DOMContentLoaded', () => {
+    const yamlEditor = document.getElementById('yaml-editor');
+    const frequencyEditor = document.getElementById('frequency-editor');
+
+    // 尝试从 LocalStorage 恢复配置
+    const savedConfig = localStorage.getItem(STORAGE_KEY_CONFIG);
+    const savedFrequency = localStorage.getItem(STORAGE_KEY_FREQUENCY);
+
+    // 初始化编辑器
+    if (savedConfig && savedConfig.trim() && savedConfig !== INITIAL_YAML) {
+        yamlEditor.value = savedConfig;
+        currentYaml = savedConfig;
+        showToast('已恢复上次保存的配置', 'info');
+    } else {
+        yamlEditor.value = INITIAL_YAML;
+        currentYaml = INITIAL_YAML;
+    }
+
+    if (savedFrequency && savedFrequency.trim()) {
+        frequencyEditor.value = savedFrequency;
+        currentFrequency = savedFrequency;
+    } else {
+        frequencyEditor.value = "# 在此粘贴你的 frequency_words.txt 内容...\n# 或拖拽文件到编辑器区域\n\n[GLOBAL_FILTER]\n\n[WORD_GROUPS]\n";
+        currentFrequency = frequencyEditor.value;
+    }
+
+    // 渲染右侧模块列表
+    renderModules();
+
+    // 监听编辑器输入(实时同步到 UI + 防抖保存)
+    yamlEditor.addEventListener('input', (e) => {
+        currentYaml = e.target.value;
+        updateBackdrop('yaml-editor', 'yaml-backdrop');
+        syncYamlToUI();
+        debounceSaveConfig();
+    });
+
+    frequencyEditor.addEventListener('input', (e) => {
+        currentFrequency = e.target.value;
+        updateBackdrop('frequency-editor', 'frequency-backdrop');
+        currentFrequencyData = null;
+        syncFrequencyToUI();
+        debounceSaveFrequency();
+    });
+
+    // 同步滚动
+    yamlEditor.addEventListener('scroll', () => syncScroll('yaml-editor', 'yaml-backdrop'));
+    frequencyEditor.addEventListener('scroll', () => syncScroll('frequency-editor', 'frequency-backdrop'));
+
+    // 初始化拖拽上传功能
+    initDragAndDrop(yamlEditor, 'config');
+    initDragAndDrop(frequencyEditor, 'frequency');
+
+    // 页面关闭/刷新时立即保存
+    window.addEventListener('beforeunload', saveAllToLocalStorage);
+
+    document.addEventListener('keydown', function(e) {
+        if ((e.ctrlKey || e.metaKey) && e.key === 's') {
+            e.preventDefault();
+            saveAllToLocalStorage();
+            showToast('已手动保存配置', 'success');
+        }
+    });
+
+    syncYamlToUI();
+
+    updateBackdrop('yaml-editor', 'yaml-backdrop');
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+
+    updateSaveTimeDisplay();
+});
+
+// 防抖保存 config.yaml
+function debounceSaveConfig() {
+    if (configSaveTimer) clearTimeout(configSaveTimer);
+    configSaveTimer = setTimeout(() => {
+        saveConfigToLocalStorage();
+    }, 1000);
+}
+
+// 防抖保存 frequency_words.txt
+function debounceSaveFrequency() {
+    if (frequencySaveTimer) clearTimeout(frequencySaveTimer);
+    frequencySaveTimer = setTimeout(() => {
+        saveFrequencyToLocalStorage();
+    }, 1000);
+}
+
+// ==========================================
+// 2.1 拖拽上传功能
+// ==========================================
+function initDragAndDrop(editor, type) {
+    const container = editor.parentElement;
+
+    const dropOverlay = document.createElement('div');
+    dropOverlay.className = 'drop-overlay hidden';
+    dropOverlay.innerHTML = `
+        <div class="drop-overlay-content">
+            <i class="fa-solid fa-cloud-arrow-up text-4xl mb-2"></i>
+            <div class="text-sm font-bold">释放以加载文件</div>
+            <div class="text-xs opacity-75">${type === 'config' ? 'config.yaml' : 'frequency_words.txt'}</div>
+        </div>
+    `;
+    container.style.position = 'relative';
+    container.appendChild(dropOverlay);
+
+    editor.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        dropOverlay.classList.remove('hidden');
+    });
+
+    editor.addEventListener('dragleave', (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (!container.contains(e.relatedTarget)) {
+            dropOverlay.classList.add('hidden');
+        }
+    });
+
+    dropOverlay.addEventListener('dragleave', (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (!container.contains(e.relatedTarget)) {
+            dropOverlay.classList.add('hidden');
+        }
+    });
+
+    dropOverlay.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+    });
+
+    dropOverlay.addEventListener('drop', (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        dropOverlay.classList.add('hidden');
+        handleFileDrop(e, type);
+    });
+
+    editor.addEventListener('drop', (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        dropOverlay.classList.add('hidden');
+        handleFileDrop(e, type);
+    });
+}
+
+function handleFileDrop(e, type) {
+    const files = e.dataTransfer.files;
+    if (files.length === 0) return;
+
+    const file = files[0];
+
+    const validExtensions = type === 'config'
+        ? ['.yaml', '.yml', '.txt']
+        : ['.txt', '.yaml', '.yml'];
+
+    const fileName = file.name.toLowerCase();
+    const isValid = validExtensions.some(ext => fileName.endsWith(ext));
+
+    if (!isValid) {
+        showToast(`请拖入 ${type === 'config' ? 'YAML' : 'TXT'} 文件`, 'error');
+        return;
+    }
+
+    const reader = new FileReader();
+    reader.onload = (event) => {
+        const content = event.target.result;
+
+        if (type === 'config') {
+            try {
+                jsyaml.load(content);
+                document.getElementById('yaml-editor').value = content;
+                currentYaml = content;
+                syncYamlToUI();
+                showToast(`已加载: ${file.name}`, 'success');
+            } catch (err) {
+                showToast(`YAML 语法错误: ${err.message}`, 'error');
+                // 仍然加载,让用户修复
+                document.getElementById('yaml-editor').value = content;
+                currentYaml = content;
+            }
+        } else {
+            document.getElementById('frequency-editor').value = content;
+            currentFrequency = content;
+            syncFrequencyToUI();
+            showToast(`已加载: ${file.name}`, 'success');
+        }
+    };
+
+    reader.onerror = () => {
+        showToast('文件读取失败', 'error');
+    };
+
+    reader.readAsText(file);
+}
+
+// ==========================================
+// 2.2 LocalStorage 保存与恢复
+// ==========================================
+
+// 保存 config.yaml
+function saveConfigToLocalStorage() {
+    try {
+        if (currentYaml && currentYaml.trim().length > 10) {
+            const now = new Date().toISOString();
+            localStorage.setItem(STORAGE_KEY_CONFIG, currentYaml);
+            localStorage.setItem(STORAGE_KEY_CONFIG_TIME, now);
+            updateSaveTimeDisplay();
+        }
+    } catch (e) {
+        console.warn('LocalStorage 保存 config 失败:', e);
+    }
+}
+
+// 保存 frequency_words.txt
+function saveFrequencyToLocalStorage() {
+    try {
+        if (currentFrequency && currentFrequency.trim().length > 10) {
+            const now = new Date().toISOString();
+            localStorage.setItem(STORAGE_KEY_FREQUENCY, currentFrequency);
+            localStorage.setItem(STORAGE_KEY_FREQUENCY_TIME, now);
+            updateSaveTimeDisplay();
+        }
+    } catch (e) {
+        console.warn('LocalStorage 保存 frequency 失败:', e);
+    }
+}
+
+// 保存全部(页面关闭时调用)
+function saveAllToLocalStorage() {
+    saveConfigToLocalStorage();
+    saveFrequencyToLocalStorage();
+}
+
+// 兼容旧调用
+function saveToLocalStorage() {
+    saveAllToLocalStorage();
+}
+
+// 格式化时间显示
+function formatSaveTime(isoString) {
+    if (!isoString) return '未保存';
+    const date = new Date(isoString);
+    const now = new Date();
+    const diffMs = now - date;
+    const diffMins = Math.floor(diffMs / 60000);
+    const diffHours = Math.floor(diffMs / 3600000);
+    const diffDays = Math.floor(diffMs / 86400000);
+
+    if (diffMins < 1) return '刚刚';
+    if (diffMins < 60) return `${diffMins} 分钟前`;
+    if (diffHours < 24) return `${diffHours} 小时前`;
+    if (diffDays < 7) return `${diffDays} 天前`;
+
+    return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+}
+
+// 更新保存时间显示
+function updateSaveTimeDisplay() {
+    const configTime = localStorage.getItem(STORAGE_KEY_CONFIG_TIME);
+    const frequencyTime = localStorage.getItem(STORAGE_KEY_FREQUENCY_TIME);
+
+    // 更新 config.yaml 的时间显示
+    const configTimeEl = document.getElementById('config-save-time');
+    const configLabelEl = document.getElementById('config-save-label');
+    if (configTimeEl) {
+        configTimeEl.textContent = formatSaveTime(configTime);
+        configTimeEl.title = configTime ? new Date(configTime).toLocaleString('zh-CN') : '未保存';
+        if (configLabelEl) {
+            if (configTime) {
+                configLabelEl.classList.remove('hidden');
+            } else {
+                configLabelEl.classList.add('hidden');
+            }
+        }
+    }
+
+    // 更新 frequency_words.txt 的时间显示
+    const frequencyTimeEl = document.getElementById('frequency-save-time');
+    const frequencyLabelEl = document.getElementById('frequency-save-label');
+    if (frequencyTimeEl) {
+        frequencyTimeEl.textContent = formatSaveTime(frequencyTime);
+        frequencyTimeEl.title = frequencyTime ? new Date(frequencyTime).toLocaleString('zh-CN') : '未保存';
+        if (frequencyLabelEl) {
+            if (frequencyTime) {
+                frequencyLabelEl.classList.remove('hidden');
+            } else {
+                frequencyLabelEl.classList.add('hidden');
+            }
+        }
+    }
+}
+
+// ==========================================
+// 2.3 加载官网最新配置
+// ==========================================
+window.openLoadConfigModal = function() {
+    // 创建选择弹窗
+    const modal = document.createElement('div');
+    modal.id = 'load-config-modal';
+    modal.className = 'modal-overlay';
+    modal.innerHTML = `
+        <div class="modal-content" style="max-width: 420px;">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-bold text-gray-800"><i class="fa-solid fa-cloud-arrow-down mr-2 text-blue-500"></i>加载官网最新配置</h3>
+                <button onclick="closeLoadConfigModal()" class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-times text-xl"></i></button>
+            </div>
+            <div class="text-sm text-gray-600 mb-4">
+                选择要从 GitHub 加载的配置文件:
+            </div>
+            <div class="space-y-3">
+                <label class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-blue-50 hover:border-blue-300 cursor-pointer transition-colors">
+                    <input type="checkbox" id="load-config-yaml" checked class="w-4 h-4 text-blue-600 rounded">
+                    <div class="flex-1">
+                        <div class="font-medium text-gray-800">config.yaml</div>
+                        <div class="text-xs text-gray-500">系统配置、平台、AI、通知等</div>
+                    </div>
+                    <i class="fa-solid fa-file-code text-blue-400"></i>
+                </label>
+                <label class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-blue-50 hover:border-blue-300 cursor-pointer transition-colors">
+                    <input type="checkbox" id="load-frequency-txt" checked class="w-4 h-4 text-blue-600 rounded">
+                    <div class="flex-1">
+                        <div class="font-medium text-gray-800">frequency_words.txt</div>
+                        <div class="text-xs text-gray-500">关键词组、过滤规则、正则逻辑</div>
+                    </div>
+                    <i class="fa-solid fa-filter text-orange-400"></i>
+                </label>
+            </div>
+            <div class="text-xs text-gray-400 mt-3 p-2 bg-gray-50 rounded">
+                <i class="fa-solid fa-info-circle mr-1"></i>
+                数据来源:<a href="https://github.com/sansan0/TrendRadar" target="_blank" class="text-blue-500 hover:underline">sansan0/TrendRadar</a>
+            </div>
+            <div class="flex justify-end gap-2 mt-4">
+                <button onclick="closeLoadConfigModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">取消</button>
+                <button onclick="confirmLoadConfig()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
+                    <i class="fa-solid fa-download mr-1"></i>加载选中
+                </button>
+            </div>
+        </div>
+    `;
+    document.body.appendChild(modal);
+}
+
+window.closeLoadConfigModal = function() {
+    const modal = document.getElementById('load-config-modal');
+    if (modal) modal.remove();
+}
+
+window.confirmLoadConfig = async function() {
+    const loadConfig = document.getElementById('load-config-yaml')?.checked;
+    const loadFrequency = document.getElementById('load-frequency-txt')?.checked;
+
+    if (!loadConfig && !loadFrequency) {
+        showToast('请至少选择一个文件', 'warning');
+        return;
+    }
+
+    closeLoadConfigModal();
+    showToast('正在从 GitHub 加载...', 'info');
+
+    try {
+        const promises = [];
+        if (loadConfig) promises.push(fetch(REMOTE_CONFIG_URL).then(r => ({ type: 'config', res: r })));
+        if (loadFrequency) promises.push(fetch(REMOTE_FREQUENCY_URL).then(r => ({ type: 'frequency', res: r })));
+
+        const results = await Promise.all(promises);
+
+        for (const { type, res } of results) {
+            if (!res.ok) {
+                throw new Error(`${type === 'config' ? 'config.yaml' : 'frequency_words.txt'} 加载失败: ${res.status}`);
+            }
+
+            const text = await res.text();
+
+            if (type === 'config') {
+                // 验证 YAML 语法
+                try {
+                    jsyaml.load(text);
+                } catch (yamlErr) {
+                    showToast(`YAML 语法错误: ${yamlErr.message}`, 'error');
+                    continue;
+                }
+                document.getElementById('yaml-editor').value = text;
+                currentYaml = text;
+                updateBackdrop('yaml-editor', 'yaml-backdrop');
+                syncYamlToUI();
+            } else {
+                document.getElementById('frequency-editor').value = text;
+                currentFrequency = text;
+                currentFrequencyData = null;
+                updateBackdrop('frequency-editor', 'frequency-backdrop');
+                syncFrequencyToUI();
+            }
+        }
+
+        saveToLocalStorage();
+
+        const loadedFiles = [];
+        if (loadConfig) loadedFiles.push('config.yaml');
+        if (loadFrequency) loadedFiles.push('frequency_words.txt');
+        showToast(`已加载: ${loadedFiles.join(', ')}`, 'success');
+
+    } catch (err) {
+        console.error('加载远程配置失败:', err);
+        showToast(`加载失败: ${err.message}`, 'error');
+    }
+}
+
+// ==========================================
+// 2.4 Toast 提示
+// ==========================================
+function showToast(message, type = 'info') {
+    // 移除已有的 toast
+    const existingToast = document.querySelector('.toast-notification');
+    if (existingToast) existingToast.remove();
+
+    const toast = document.createElement('div');
+    toast.className = `toast-notification toast-${type}`;
+
+    const icons = {
+        success: 'fa-check-circle',
+        error: 'fa-times-circle',
+        info: 'fa-info-circle',
+        warning: 'fa-exclamation-triangle'
+    };
+
+    toast.innerHTML = `
+        <i class="fa-solid ${icons[type] || icons.info}"></i>
+        <span>${message}</span>
+    `;
+
+    document.body.appendChild(toast);
+
+    // 动画入场
+    requestAnimationFrame(() => {
+        toast.classList.add('show');
+    });
+
+    // 自动消失
+    setTimeout(() => {
+        toast.classList.remove('show');
+        setTimeout(() => toast.remove(), 300);
+    }, 3000);
+}
+
+// ==========================================
+// 3. 渲染逻辑
+// ==========================================
+function renderModules() {
+    const container = document.getElementById('config-panel');
+    container.innerHTML = '';
+
+    renderModuleNav();
+
+    MODULE_DEFS.forEach(mod => {
+        const card = document.createElement('div');
+        card.className = `module-card ${mod.editable ? 'active' : 'disabled'}`;
+        card.id = `module-${mod.key}`;
+
+        const header = `
+            <div class="module-header px-4 py-3 flex items-center justify-between cursor-pointer" onclick="scrollToModuleInEditor('${mod.key}')">
+                <div class="flex items-center">
+                    <span class="text-sm font-bold">${mod.name}</span>
+                    <i class="fa-solid fa-arrow-up-right-from-square text-blue-400 text-[10px] ml-2 opacity-0 group-hover:opacity-100" title="跳转到左侧编辑器"></i>
+                </div>
+                ${!mod.editable ?
+                    '<span class="locked-badge text-[10px] text-gray-400 border border-gray-200 px-1.5 py-0.5 rounded">只读 (请在左侧编辑)</span>' :
+                    '<i class="fa-solid fa-chevron-down text-gray-400 text-xs"></i>'}
+            </div>
+        `;
+
+        const body = mod.editable ? `<div class="module-body p-5 border-t border-gray-50 space-y-4" id="controls-${mod.key}"></div>` : '';
+
+        card.innerHTML = header + body;
+        container.appendChild(card);
+
+        if (mod.editable) {
+            renderControls(mod);
+        }
+    });
+}
+
+// 渲染模块导航栏
+function renderModuleNav() {
+    const nav = document.getElementById('module-nav');
+    if (!nav) return;
+
+    nav.innerHTML = MODULE_DEFS.map(mod => `
+        <button onclick="scrollToModuleInEditor('${mod.key}')"
+                class="module-nav-btn text-[10px] px-2 py-1 rounded ${mod.editable ? 'bg-blue-100 text-blue-700 hover:bg-blue-200' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'} transition-colors"
+                title="跳转到模块 ${mod.id}">
+            ${mod.id}
+        </button>
+    `).join('');
+}
+
+// 切换组名编辑状态
+window.toggleGroupNameEdit = function(btn) {
+    const container = btn.parentNode;
+    const span = container.querySelector('span.text-sm');
+    const input = container.querySelector('input[type="text"]');
+
+    if (input.classList.contains('hidden')) {
+        // 进入编辑模式
+        span.classList.add('hidden');
+        input.classList.remove('hidden');
+        input.focus();
+        btn.innerHTML = '<i class="fa-solid fa-check text-green-600"></i>';
+    } else {
+        // 退出编辑模式
+        span.classList.remove('hidden');
+        input.classList.add('hidden');
+        btn.innerHTML = '<i class="fa-solid fa-pen"></i>';
+
+        // 如果内容变化,已经通过 onchange 触发更新
+        span.textContent = input.value;
+    }
+}
+
+// 跳转到左侧编辑器中对应词组的位置
+window.scrollToWordGroupInEditor = function(groupIndex) {
+    const editor = document.getElementById('frequency-editor');
+    // 重新解析以确保行号准确
+    const data = parseFrequencyText(editor.value);
+
+    if (!data.wordGroups[groupIndex]) return;
+
+    const targetLineIndex = data.wordGroups[groupIndex].startLine;
+    if (targetLineIndex === undefined || targetLineIndex === -1) return;
+
+    const lines = editor.value.split('\n');
+    const lineHeight = 19.5;
+    const scrollPosition = targetLineIndex * lineHeight;
+
+    // 设置光标选区
+    let charCount = 0;
+    for (let i = 0; i < targetLineIndex; i++) {
+        charCount += lines[i].length + 1; // +1 for newline
+    }
+
+    editor.focus();
+    editor.setSelectionRange(charCount, charCount + lines[targetLineIndex].length);
+    editor.scrollTop = scrollPosition - 50;
+
+    // 高亮效果
+    editor.style.transition = 'background-color 0.3s';
+    const originalBg = editor.style.backgroundColor;
+    editor.style.backgroundColor = '#2d4a7c';
+    setTimeout(() => {
+        editor.style.backgroundColor = originalBg;
+    }, 300);
+}
+
+// 跳转到左侧编辑器中对应模块的位置
+window.scrollToModuleInEditor = function(modKey) {
+    const editor = document.getElementById('yaml-editor');
+    const yaml = editor.value;
+    const lines = yaml.split('\n');
+
+    // 查找模块标题注释行(# N. 模块名)
+    let targetLineIndex = -1;
+    const mod = MODULE_DEFS.find(m => m.key === modKey);
+    if (!mod) return;
+
+    // 直接匹配包含模块编号的标题行,如:# 5. 推送内容控制
+    const moduleTitlePattern = new RegExp(`^#\\s*${mod.id}\\.\\s+`, 'i');
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        // 匹配模块标题行(包含编号的注释行)
+        if (moduleTitlePattern.test(line)) {
+            targetLineIndex = i;
+            break;
+        }
+    }
+
+    // 如果没找到标题行,尝试查找模块键名(如 platforms:)
+    if (targetLineIndex === -1) {
+        for (let i = 0; i < lines.length; i++) {
+            if (lines[i].match(new RegExp(`^${modKey}:\\s*`))) {
+                targetLineIndex = i;
+                break;
+            }
+        }
+    }
+
+    if (targetLineIndex === -1) return;
+
+    // 计算目标位置并滚动
+    const lineHeight = 19.5;
+    const scrollPosition = targetLineIndex * lineHeight;
+
+    // 设置光标位置
+    const textBeforeTarget = lines.slice(0, targetLineIndex).join('\n').length + (targetLineIndex > 0 ? 1 : 0);
+    editor.focus();
+    editor.setSelectionRange(textBeforeTarget, textBeforeTarget + lines[targetLineIndex].length);
+
+    editor.scrollTop = scrollPosition - 5;
+
+    // 高亮提示(闪烁效果)
+    editor.style.transition = 'background-color 0.3s';
+    const originalBg = editor.style.backgroundColor;
+    editor.style.backgroundColor = '#2d4a7c';
+    setTimeout(() => {
+        editor.style.backgroundColor = originalBg;
+    }, 300);
+}
+
+function renderControls(mod) {
+    const body = document.getElementById(`controls-${mod.key}`);
+
+    // 根据模块 key 定义不同的 UI 控件
+    let html = "";
+
+    switch(mod.key) {
+        case "platforms":
+            html = createToggleControl(mod.key, "enabled", "启用热榜抓取");
+            html += `<div class="mt-4 mb-2 text-xs font-bold text-gray-700">平台列表 <span class="text-gray-400 font-normal">(可拖拽排序)</span></div>`;
+            html += `<div id="platforms-list" class="space-y-2"></div>`;
+            html += `<div class="flex items-center gap-2 mt-3">
+                        <button onclick="openPlatformModal()" class="text-xs bg-green-600 text-white px-3 py-1.5 rounded hover:bg-green-700 transition-colors">
+                            <i class="fa-solid fa-plus mr-1"></i>添加平台
+                        </button>
+                        <a href="https://github.com/sansan0/TrendRadar?tab=readme-ov-file#%E9%85%8D%E7%BD%AE%E8%AF%A6%E8%A7%A3" target="_blank" class="text-xs bg-gray-100 text-gray-600 px-3 py-1.5 rounded hover:bg-gray-200 transition-colors border border-gray-200 flex items-center gap-1 no-underline">
+                            <i class="fa-solid fa-circle-question text-gray-400"></i>添加其它平台
+                        </a>
+                     </div>`;
+            break;
+        case "rss":
+            html = createToggleControl(mod.key, "enabled", "启用 RSS 抓取");
+            html += `<div class="mt-3 mb-2 text-xs font-bold text-gray-700">新鲜度过滤</div>`;
+            html += createToggleControl(mod.key, "freshness_filter.enabled", "启用新鲜度过滤");
+            html += createNumberControl(mod.key, "freshness_filter.max_age_days", "最大文章年龄 (天)");
+            html += `<div class="mt-4 mb-2 text-xs font-bold text-gray-700">RSS 源列表</div>`;
+            html += `<div id="rss-feeds-list" class="space-y-2"></div>`;
+            html += `<div class="flex items-center gap-2 mt-3">
+                        <button onclick="openRssModal()" class="text-xs bg-green-600 text-white px-3 py-1.5 rounded hover:bg-green-700 transition-colors">
+                            <i class="fa-solid fa-plus mr-1"></i>添加 RSS 源
+                        </button>
+                        <div class="text-xs text-gray-500 italic">
+                            (内附 RSS 源参考库)
+                        </div>
+                     </div>`;
+            html += `<div class="text-xs text-orange-600 mt-2 p-2 bg-orange-50 rounded border border-orange-200">
+                        <i class="fa-solid fa-triangle-exclamation mr-1"></i>
+                        <strong>注意:</strong>部分海外媒体内容可能涉及敏感话题,AI 模型可能拒绝翻译或分析,建议根据实际需求筛选订阅源。
+                     </div>`;
+            break;
+        case "report":
+            html = createSelectControl(mod.key, "mode", "报告模式", ["current", "daily", "incremental"]);
+            html += createSelectControl(mod.key, "display_mode", "分组维度", ["keyword", "platform"]);
+            html += createToggleControl(mod.key, "sort_by_position_first", "按定义顺序排序");
+            html += createNumberControl(mod.key, "rank_threshold", "排名高亮阈值");
+            html += createNumberControl(mod.key, "max_news_per_keyword", "每个关键词最大显示数量");
+            break;
+        case "display":
+            html = `<div class="text-xs font-bold text-gray-700 mb-2">推送内容控制 <span class="text-gray-400 font-normal">(可拖拽排序)</span></div>`;
+            html += `<div id="display-regions-list" class="space-y-2"></div>`;
+            html += `<div class="text-xs text-gray-500 mt-2 mb-6">
+                        <i class="fa-solid fa-lightbulb mr-1"></i>
+                        提示:列表顺序决定了报告中的显示顺序
+                     </div>`;
+
+            // Standalone Configuration Section
+            html += `<div class="border-t border-gray-200 pt-4 mt-4">`;
+            html += `<div class="text-xs font-bold text-gray-700 mb-3">独立展示区配置 <span class="text-gray-400 font-normal">(仅在上方开启"独立展示区"时生效)</span></div>`;
+
+            html += createNumberControl(mod.key, "standalone.max_items", "每个源最多展示条数");
+
+            html += `<div class="mt-3 mb-2 text-xs font-medium text-gray-700">选择要展示的热榜平台</div>`;
+            html += `<div id="standalone-platforms-list" class="max-h-40 overflow-y-auto border border-gray-200 rounded p-2 bg-gray-50 grid grid-cols-2 gap-2"></div>`;
+
+            html += `<div class="mt-3 mb-2 text-xs font-medium text-gray-700">选择要展示的 RSS 源</div>`;
+            html += `<div id="standalone-rss-list" class="max-h-40 overflow-y-auto border border-gray-200 rounded p-2 bg-gray-50 grid grid-cols-1 gap-2"></div>`;
+
+            html += `</div>`;
+
+            setTimeout(() => {
+                renderDisplayRegionsList();
+                renderStandaloneLists();
+            }, 0);
+            break;
+        case "notification":
+            // 只有推送窗口可见
+            html = `<div class="text-xs font-bold text-blue-600 mb-2">推送时间窗口设置</div>`;
+            html += createToggleControl(mod.key, "push_window.enabled", "开启时间窗口");
+            html += `<div class="grid grid-cols-2 gap-4">
+                        ${createInputControl(mod.key, "push_window.start", "开始时间 (HH:MM)")}
+                        ${createInputControl(mod.key, "push_window.end", "结束时间 (HH:MM)")}
+                    </div>`;
+            html += createToggleControl(mod.key, "push_window.once_per_day", "窗口内仅推送一次");
+            html += `<div class="text-xs text-gray-500 mt-2">通知渠道配置请在左侧编辑器中修改</div>`;
+            break;
+        case "ai":
+            html = createInputControl(mod.key, "model", "模型名称");
+            html += createInputControl(mod.key, "api_key", "API Key", "password");
+            html += createInputControl(mod.key, "api_base", "API Base URL (可选)");
+            html += createNumberControl(mod.key, "timeout", "请求超时 (秒)");
+            html += createNumberControl(mod.key, "temperature", "采样温度 (0.0-2.0)");
+            html += createNumberControl(mod.key, "max_tokens", "最大生成 Token 数");
+            break;
+        case "ai_analysis":
+            html = createToggleControl(mod.key, "enabled", "开启 AI 分析报告");
+
+            // AI 分析时间窗口设置
+            html += `<div class="text-xs font-bold text-blue-600 mb-2 mt-4">AI 分析时间窗口设置</div>`;
+            html += createToggleControl(mod.key, "analysis_window.enabled", "开启时间窗口");
+            html += `<div class="grid grid-cols-2 gap-4">
+                        ${createInputControl(mod.key, "analysis_window.start", "开始时间 (HH:MM)")}
+                        ${createInputControl(mod.key, "analysis_window.end", "结束时间 (HH:MM)")}
+                    </div>`;
+            html += createToggleControl(mod.key, "analysis_window.once_per_day", "窗口内仅分析一次");
+
+            // 其他 AI 分析配置
+            html += `<div class="text-xs font-bold text-blue-600 mb-2 mt-4">分析内容配置</div>`;
+            html += createInputControl(mod.key, "language", "输出语言");
+            html += createInputControl(mod.key, "prompt_file", "提示词配置文件");
+            html += createSelectControl(mod.key, "mode", "AI 分析模式", ["follow_report", "daily", "current", "incremental"]);
+            html += createNumberControl(mod.key, "max_news_for_analysis", "最大分析条数");
+            html += createToggleControl(mod.key, "include_rss", "包含 RSS 内容");
+            html += createToggleControl(mod.key, "include_rank_timeline", "传递完整排名时间线");
+            break;
+        case "ai_translation":
+            html = createToggleControl(mod.key, "enabled", "开启 AI 自动翻译");
+            html += createInputControl(mod.key, "language", "目标语言");
+            html += createInputControl(mod.key, "prompt_file", "提示词配置文件");
+            break;
+    }
+
+    body.innerHTML = html;
+
+    // 绑定事件
+    body.querySelectorAll('input, select').forEach(el => {
+        el.addEventListener('change', (e) => {
+            updateYamlFromUI(mod.key, e.target.dataset.path, e.target);
+        });
+    });
+}
+
+// ==========================================
+// 4. 同步逻辑 (YAML -> UI)
+// ==========================================
+function syncYamlToUI() {
+    try {
+        const doc = jsyaml.load(currentYaml);
+        if (!doc) return;
+
+        MODULE_DEFS.filter(m => m.editable).forEach(mod => {
+            const modData = doc[mod.key];
+            if (!modData) return;
+
+            const controls = document.querySelectorAll(`#controls-${mod.key} [data-path]`);
+            controls.forEach(ctrl => {
+                const path = ctrl.dataset.path.split('.');
+                let val = modData;
+                for (const part of path) {
+                    val = val ? val[part] : undefined;
+                }
+
+                if (ctrl.type === 'checkbox') {
+                    ctrl.checked = !!val;
+                } else {
+                    ctrl.value = val !== undefined ? val : "";
+                }
+            });
+        });
+
+        renderPlatformsList();
+        renderRssFeedsList();
+        renderStandaloneLists(); 
+    } catch (e) {
+        // 解析失败时不更新 UI,保持原有状态
+    }
+}
+
+// ==========================================
+// 5. 更新逻辑 (UI -> YAML) - 核心难点:正则保留注释
+// ==========================================
+function updateYamlFromUI(modKey, path, el) {
+    let newVal = el.type === 'checkbox' ? el.checked : el.value;
+
+    // 如果是数字类型
+    if (el.type === 'number') {
+        newVal = parseFloat(newVal);
+        if (isNaN(newVal)) newVal = 0;
+    }
+
+    const editor = document.getElementById('yaml-editor');
+    let yaml = editor.value;
+    const lines = yaml.split('\n');
+    const pathParts = path.split('.');
+
+    // 找到模块的起始行
+    let moduleStartLine = -1;
+    let moduleEndLine = lines.length;
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        // 匹配模块开始(非缩进的 key:)
+        const moduleMatch = line.match(/^([a-z_]+):/);
+        if (moduleMatch) {
+            if (moduleMatch[1] === modKey) {
+                moduleStartLine = i;
+            } else if (moduleStartLine >= 0) {
+                // 找到下一个模块,记录当前模块结束位置
+                moduleEndLine = i;
+                break;
+            }
+        }
+    }
+
+    if (moduleStartLine < 0) return;
+
+    // 在模块内查找目标路径
+    let targetLine = -1;
+    let currentIndent = 0;
+    let searchKey = pathParts[pathParts.length - 1];
+
+    for (let i = moduleStartLine + 1; i < moduleEndLine; i++) {
+        const line = lines[i];
+        if (line.trim() === '' || line.trim().startsWith('#')) continue;
+
+        // 检查是否匹配目标键
+        const indent = line.search(/\S/);
+        const keyMatch = line.match(/^\s*([a-z_]+):\s*(.*)/i);
+
+        if (keyMatch && keyMatch[1] === searchKey) {
+            // 如果是嵌套路径,需要检查缩进层级是否正确
+            if (pathParts.length > 1) {
+                // 简化处理:对于嵌套路径,确保在正确的父级下
+                let valid = true;
+                for (let j = 0; j < pathParts.length - 1; j++) {
+                    let found = false;
+                    for (let k = moduleStartLine + 1; k < i; k++) {
+                        const parentMatch = lines[k].match(/^\s*([a-z_]+):/i);
+                        if (parentMatch && parentMatch[1] === pathParts[j]) {
+                            found = true;
+                            break;
+                        }
+                    }
+                    if (!found) {
+                        valid = false;
+                        break;
+                    }
+                }
+                if (!valid) continue;
+            }
+
+            targetLine = i;
+            break;
+        }
+    }
+
+    if (targetLine < 0) return;
+
+    // 更新该行,保留注释
+    const originalLine = lines[targetLine];
+    const match = originalLine.match(/^(\s*[a-z_]+:\s*)(.*)$/i);
+
+    if (match) {
+        const prefix = match[1];
+        const rest = match[2];
+
+        // 提取原有注释
+        const commentMatch = rest.match(/(\s*#.*)$/);
+        const comment = commentMatch ? commentMatch[1] : '';
+
+        // 格式化新值
+        let formattedVal = newVal;
+        if (typeof newVal === 'string') {
+            // 获取原值部分(去除注释后的部分)
+            const valPart = rest.slice(0, rest.length - comment.length).trim();
+            // 检查原值是否带有引号
+            const isOriginalQuoted = (valPart.startsWith('"') && valPart.endsWith('"')) ||
+                                     (valPart.startsWith("'") && valPart.endsWith("'"));
+
+            // 如果原值有引号,或者新值包含特殊字符(空格、冒号、井号、引号)或者是空字符串,则添加双引号
+            if (isOriginalQuoted || newVal.includes(':') || newVal.includes('#') ||
+                newVal.includes('"') || newVal.includes(' ') || newVal === "") {
+                formattedVal = `"${newVal.replace(/"/g, '\\"')}"`;
+            }
+        }
+
+        // 构建新行
+        lines[targetLine] = `${prefix}${formattedVal}${comment}`;
+    }
+
+    // 更新编辑器
+    editor.value = lines.join('\n');
+    currentYaml = editor.value;
+    updateBackdrop('yaml-editor', 'yaml-backdrop');
+    debounceSaveConfig();
+}
+
+// ==========================================
+// 6. UI 组件工厂
+// ==========================================
+function createToggleControl(mod, path, label) {
+    const id = `toggle-${mod}-${path.replace('.', '-')}`;
+    return `
+        <div class="flex items-center justify-between">
+            <label for="${id}" class="text-xs font-medium text-gray-700">${label}</label>
+            <div class="relative inline-block w-10 mr-2 align-middle select-none">
+                <input type="checkbox" id="${id}" data-path="${path}" class="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer transition-all duration-200 ease-in-out"/>
+                <label for="${id}" class="toggle-label block overflow-hidden h-5 rounded-full bg-gray-300 cursor-pointer"></label>
+            </div>
+        </div>
+    `;
+}
+
+function createInputControl(mod, path, label, type = "text") {
+    return `
+        <div>
+            <label class="block text-[10px] uppercase tracking-wider font-bold text-gray-400 mb-1">${label}</label>
+            <input type="${type}" data-path="${path}" class="bg-white border-gray-300 focus:border-blue-500" placeholder="未设置">
+        </div>
+    `;
+}
+
+function createNumberControl(mod, path, label) {
+    return `
+        <div class="flex items-center justify-between">
+            <label class="text-xs font-medium text-gray-700">${label}</label>
+            <input type="number" data-path="${path}" class="w-20 text-right bg-white border-gray-300" style="width: 80px">
+        </div>
+    `;
+}
+
+function createSelectControl(mod, path, label, options) {
+    const optionsHtml = options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
+    return `
+        <div>
+            <label class="block text-[10px] uppercase tracking-wider font-bold text-gray-400 mb-1">${label}</label>
+            <select data-path="${path}" class="bg-white border-gray-300">
+                ${optionsHtml}
+            </select>
+        </div>
+    `;
+}
+
+// ==========================================
+// 7. 工具函数
+// ==========================================
+
+window.copyResult = function() {
+    const yamlEditor = document.getElementById('yaml-editor');
+    const frequencyEditor = document.getElementById('frequency-editor');
+    const editor = currentTab === 'config' ? yamlEditor : frequencyEditor;
+
+    editor.select();
+    document.execCommand('copy');
+
+    const btn = document.querySelector('button[onclick="copyResult()"]');
+    const original = btn.innerHTML;
+    btn.innerHTML = '<i class="fa-solid fa-check mr-1.5"></i>已复制!';
+    setTimeout(() => btn.innerHTML = original, 2000);
+}
+
+window.resetToDefault = function() {
+    if (confirm('确定要重置为初始状态吗?未保存的修改将丢失。')) {
+        const yamlEditor = document.getElementById('yaml-editor');
+        const frequencyEditor = document.getElementById('frequency-editor');
+
+        if (currentTab === 'config') {
+            yamlEditor.value = INITIAL_YAML;
+            currentYaml = INITIAL_YAML;
+            updateBackdrop('yaml-editor', 'yaml-backdrop');
+
+            // 清除 LocalStorage 中的 config 数据
+            localStorage.removeItem(STORAGE_KEY_CONFIG);
+            localStorage.removeItem(STORAGE_KEY_CONFIG_TIME);
+
+            // 重置 UI:重新渲染模块以清空输入框,因为 INITIAL_YAML 可能为空导致 syncYamlToUI 不执行更新
+            renderModules();
+            syncYamlToUI();
+            updateSaveTimeDisplay();
+        } else {
+            frequencyEditor.value = "# 在此粘贴你的 frequency_words.txt 内容...\n\n[GLOBAL_FILTER]\n\n[WORD_GROUPS]\n";
+            currentFrequency = frequencyEditor.value;
+            updateBackdrop('frequency-editor', 'frequency-backdrop');
+
+            // 清除 LocalStorage 中的 frequency 数据
+            localStorage.removeItem(STORAGE_KEY_FREQUENCY);
+            localStorage.removeItem(STORAGE_KEY_FREQUENCY_TIME);
+
+            syncFrequencyToUI();
+            updateSaveTimeDisplay();
+        }
+        showToast('已重置为初始状态', 'success');
+    }
+}
+
+// ==========================================
+// 8. Tab 切换功能
+// ==========================================
+window.switchTab = function(tab) {
+    currentTab = tab;
+
+    // 更新 Tab 按钮状态
+    document.getElementById('tab-config').classList.toggle('active', tab === 'config');
+    document.getElementById('tab-frequency').classList.toggle('active', tab === 'frequency');
+
+    const configBtn = document.getElementById('tab-config');
+    const freqBtn = document.getElementById('tab-frequency');
+
+    if (tab === 'config') {
+        configBtn.className = "tab-button active px-4 py-2 text-xs font-bold text-gray-300 hover:bg-[#2d2d30] transition-colors border-b-2 border-blue-500";
+        freqBtn.className = "tab-button px-4 py-2 text-xs font-bold text-gray-500 hover:bg-[#2d2d30] transition-colors border-b-2 border-transparent";
+    } else {
+        configBtn.className = "tab-button px-4 py-2 text-xs font-bold text-gray-500 hover:bg-[#2d2d30] transition-colors border-b-2 border-transparent";
+        freqBtn.className = "tab-button active px-4 py-2 text-xs font-bold text-gray-300 hover:bg-[#2d2d30] transition-colors border-b-2 border-blue-500";
+    }
+
+    // 更新编辑器显示
+    document.getElementById('yaml-editor-wrap').classList.toggle('hidden', tab !== 'config');
+    document.getElementById('frequency-editor-wrap').classList.toggle('hidden', tab !== 'frequency');
+
+    // 更新右侧面板
+    document.getElementById('config-panel').classList.toggle('hidden', tab !== 'config');
+    document.getElementById('frequency-panel').classList.toggle('hidden', tab !== 'frequency');
+
+    // 更新模块导航栏显示状态:只在 config 模式下显示
+    const moduleNav = document.getElementById('module-nav');
+    if (moduleNav) {
+        moduleNav.classList.toggle('hidden', tab !== 'config');
+    }
+
+    // 更新保存时间显示
+    const saveTimeConfig = document.getElementById('save-time-config');
+    const saveTimeFrequency = document.getElementById('save-time-frequency');
+    if (saveTimeConfig) saveTimeConfig.classList.toggle('hidden', tab !== 'config');
+    if (saveTimeFrequency) saveTimeFrequency.classList.toggle('hidden', tab !== 'frequency');
+
+    // 更新右侧标题
+    const versionBtn = document.getElementById('version-check-btn');
+    if (tab === 'config') {
+        document.getElementById('right-panel-title').textContent = '配置模块';
+        if (versionBtn) versionBtn.title = "检测 config.yaml 版本";
+    } else {
+        document.getElementById('right-panel-title').textContent = '频率词编辑';
+        if (versionBtn) versionBtn.title = "检测 frequency_words.txt 版本";
+    }
+
+    if (tab === 'frequency') {
+        renderFrequencyPanel();
+    }
+}
+
+// ==========================================
+// 9. Frequency 编辑器功能
+// ==========================================
+function parseFrequencyText(text) {
+    const result = {
+        globalFilter: [],
+        wordGroups: [],
+        originalText: text  // 保存原始文本
+    };
+
+    const lines = text.split('\n');
+    let currentSection = null;
+    let currentGroup = null;
+    let lastLineWasAlias = false;  // 追踪上一行是否为别名行
+    let relatedGroupsBuffer = [];  // 缓存连续的相关组
+    let pendingComments = [];  // 缓存待分配的注释行
+
+    // 辅助函数:保存缓存的相关组
+    function flushRelatedGroups() {
+        if (relatedGroupsBuffer.length > 0) {
+            // 如果有多个连续的组,标记它们为相关组
+            if (relatedGroupsBuffer.length > 1) {
+                relatedGroupsBuffer.forEach((group, idx) => {
+                    group.isRelatedGroup = true;
+                    group.relatedGroupIndex = idx;
+                    group.relatedGroupTotal = relatedGroupsBuffer.length;
+                });
+            }
+            result.wordGroups.push(...relatedGroupsBuffer);
+            relatedGroupsBuffer = [];
+        }
+    }
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        const trimmed = line.trim();
+
+        // 收集注释行(在 [WORD_GROUPS] 区域内)
+        if (trimmed.startsWith('#') && currentSection === 'groups') {
+            pendingComments.push(line);
+            continue;
+        }
+
+        // 跳过注释(非 [WORD_GROUPS] 区域)
+        if (trimmed.startsWith('#')) continue;
+
+        // 空行:结束当前词组和相关组缓存
+        if (!trimmed) {
+            if (currentGroup) {
+                // 保存当前词组到缓存
+                relatedGroupsBuffer.push(currentGroup);
+                currentGroup = null;
+            }
+            // 空行表示相关组结束,刷新缓存
+            flushRelatedGroups();
+            lastLineWasAlias = false;
+            // 在 [WORD_GROUPS] 区域内,空行加入待分配注释(保留空行结构)
+            if (currentSection === 'groups') {
+                pendingComments.push('');
+            }
+            continue;
+        }
+
+        // 检测区域标记
+        if (trimmed === '[GLOBAL_FILTER]') {
+            currentSection = 'global';
+            continue;
+        }
+        if (trimmed === '[WORD_GROUPS]') {
+            currentSection = 'groups';
+            continue;
+        }
+
+        // 处理内容
+        if (currentSection === 'global') {
+            result.globalFilter.push(trimmed);
+        } else if (currentSection === 'groups') {
+            // 检测组别名 [组名]
+            const groupNameMatch = trimmed.match(/^\[([^\]]+)\]$/);
+            if (groupNameMatch && !['GLOBAL_FILTER', 'WORD_GROUPS'].includes(groupNameMatch[1])) {
+                // 保存当前词组到缓存
+                if (currentGroup) {
+                    relatedGroupsBuffer.push(currentGroup);
+                }
+                // 刷新缓存(组别名独立成组)
+                flushRelatedGroups();
+                // 创建组别名类型
+                currentGroup = {
+                    type: 'group-name',
+                    name: groupNameMatch[1],
+                    keywords: [],
+                    startLine: i,
+                    precedingComments: pendingComments.length > 0 ? [...pendingComments] : []
+                };
+                pendingComments = [];
+                lastLineWasAlias = false;
+            } else {
+                // 检测 => 别名语法(允许右侧为空)
+                const aliasMatch = trimmed.match(/^(.+?)\s*=>\s*(.*)$/);
+                if (aliasMatch) {
+                    const keyword = aliasMatch[1].trim();
+                    const alias = aliasMatch[2].trim();
+
+                    // 关键逻辑:如果上一行也是别名行(无空行分隔),则归入连续别名组
+                    if (lastLineWasAlias && currentGroup && (currentGroup.type === 'alias' || currentGroup.type === 'alias-group')) {
+                        // 如果当前是单个别名,升级为别名组
+                        if (currentGroup.type === 'alias') {
+                            currentGroup.type = 'alias-group';
+                        }
+                        // 添加到别名组
+                        currentGroup.items.push({ keyword, alias });
+                    } else {
+                        // 新的单个别名(可能会升级为别名组)
+                        if (currentGroup) {
+                            // 保存当前词组到缓存(而不是直接添加到结果)
+                            relatedGroupsBuffer.push(currentGroup);
+                        }
+                        currentGroup = {
+                            type: 'alias',
+                            items: [{ keyword, alias }],
+                            startLine: i,
+                            precedingComments: pendingComments.length > 0 ? [...pendingComments] : []
+                        };
+                        pendingComments = [];
+                    }
+                    lastLineWasAlias = true;
+                } else {
+                    // 普通关键词
+                    if (!currentGroup || currentGroup.type === 'alias' || currentGroup.type === 'alias-group') {
+                        // 如果当前是别名类型,需要先保存到缓存
+                        if (currentGroup) {
+                            relatedGroupsBuffer.push(currentGroup);
+                        }
+                        // 创建新的普通词组
+                        currentGroup = {
+                            type: 'plain',
+                            keywords: [],
+                            startLine: i,
+                            precedingComments: pendingComments.length > 0 ? [...pendingComments] : []
+                        };
+                        pendingComments = [];
+                    }
+                    currentGroup.keywords.push(trimmed);
+                    lastLineWasAlias = false;
+                }
+            }
+        }
+    }
+
+    // 添加最后一个组
+    if (currentGroup) {
+        relatedGroupsBuffer.push(currentGroup);
+    }
+    flushRelatedGroups();
+
+    return result;
+}
+
+function buildFrequencyText(data) {
+    // 如果有原始文本,尝试保留注释
+    if (data.originalText) {
+        const lines = data.originalText.split('\n');
+        let result = [];
+
+        // 第一步:保留文件头部的注释
+        let i = 0;
+        while (i < lines.length) {
+            const line = lines[i];
+            const trimmed = line.trim();
+
+            if (trimmed === '[GLOBAL_FILTER]') {
+                break;
+            }
+            result.push(line);
+            i++;
+        }
+
+        // 第二步:重建 [GLOBAL_FILTER] 区域
+        result.push('[GLOBAL_FILTER]');
+
+        // 保留 [GLOBAL_FILTER] 后面的注释(直到第一个非注释非空行)
+        i++;
+        while (i < lines.length) {
+            const line = lines[i];
+            const trimmed = line.trim();
+            if (trimmed.startsWith('#') || trimmed === '') {
+                result.push(line);
+                i++;
+            } else {
+                break;
+            }
+        }
+
+        // 添加全局过滤词
+        data.globalFilter.forEach(filter => {
+            result.push(filter);
+        });
+        result.push('');
+        result.push('');
+
+        // 跳过原始文件中的 [GLOBAL_FILTER] 内容(非注释行),保留注释直到 [WORD_GROUPS]
+        while (i < lines.length) {
+            const line = lines[i];
+            const trimmed = line.trim();
+            if (trimmed === '[WORD_GROUPS]') {
+                break;
+            }
+            // 保留注释和空行
+            if (trimmed.startsWith('#') || trimmed === '') {
+                result.push(line);
+            }
+            i++;
+        }
+
+        // 第三步:重建 [WORD_GROUPS] 区域
+        result.push('[WORD_GROUPS]');
+        result.push('');
+
+        // 添加词组(注释已保存在每个词组的 precedingComments 中)
+        data.wordGroups.forEach((group, index) => {
+            // 先输出词组前的注释
+            if (group.precedingComments && group.precedingComments.length > 0) {
+                group.precedingComments.forEach(comment => {
+                    result.push(comment);
+                });
+            }
+
+            if (group.type === 'group-name') {
+                // 组别名类型:[组名] + 关键词
+                if (group.name) {
+                    result.push(`[${group.name}]`);
+                }
+                group.keywords.forEach(kw => {
+                    result.push(kw);
+                });
+            } else if (group.type === 'alias' || group.type === 'alias-group') {
+                // 别名类型:keyword => alias
+                group.items.forEach(item => {
+                    result.push(`${item.keyword} => ${item.alias}`);
+                });
+            } else if (group.type === 'plain') {
+                // 普通词组
+                group.keywords.forEach(kw => {
+                    result.push(kw);
+                });
+            }
+
+            // 空行处理逻辑:
+            // 1. 如果当前词组和下一个词组都是相关组,则不添加空行
+            // 2. 否则,在词组之间添加空行
+            const isLastGroup = index === data.wordGroups.length - 1;
+            const nextGroup = !isLastGroup ? data.wordGroups[index + 1] : null;
+
+            // 简化判断:只要当前和下一个都是相关组,就不添加空行
+            const bothAreRelatedGroups = group.isRelatedGroup && nextGroup && nextGroup.isRelatedGroup;
+
+            // 如果下一个词组有前置注释,不需要额外添加空行(注释中已包含空行)
+            const nextHasComments = nextGroup && nextGroup.precedingComments && nextGroup.precedingComments.length > 0;
+
+            if (bothAreRelatedGroups) {
+                // 相关组内部不添加空行
+                // 不添加任何内容
+            } else if (!isLastGroup && !nextHasComments) {
+                // 词组之间添加空行(如果下一个没有前置注释)
+                result.push('');
+            } else if (isLastGroup) {
+                // 最后一个词组后也保留一个空行
+                result.push('');
+            }
+        });
+
+        return result.join('\n');
+    }
+
+    // 如果没有原始文本,使用默认模板
+    let text = '# ═══════════════════════════════════════════════════════════════\n';
+    text += '#                    TrendRadar 频率词配置文件\n';
+    text += '# ═══════════════════════════════════════════════════════════════\n\n';
+
+    text += '[GLOBAL_FILTER]\n';
+    data.globalFilter.forEach(filter => {
+        text += filter + '\n';
+    });
+    text += '\n\n';
+
+    text += '[WORD_GROUPS]\n\n';
+    data.wordGroups.forEach((group, index) => {
+        // 先输出词组前的注释
+        if (group.precedingComments && group.precedingComments.length > 0) {
+            group.precedingComments.forEach(comment => {
+                text += comment + '\n';
+            });
+        }
+
+        if (group.type === 'group-name') {
+            if (group.name) {
+                text += `[${group.name}]\n`;
+            }
+            group.keywords.forEach(kw => {
+                text += kw + '\n';
+            });
+        } else if (group.type === 'alias' || group.type === 'alias-group') {
+            group.items.forEach(item => {
+                text += `${item.keyword} => ${item.alias}\n`;
+            });
+        } else if (group.type === 'plain') {
+            group.keywords.forEach(kw => {
+                text += kw + '\n';
+            });
+        }
+
+        // 空行处理逻辑:与上面保持一致
+        const isLastGroup = index === data.wordGroups.length - 1;
+        const nextGroup = !isLastGroup ? data.wordGroups[index + 1] : null;
+
+        const bothAreRelatedGroups = group.isRelatedGroup && nextGroup && nextGroup.isRelatedGroup;
+
+        // 如果下一个词组有前置注释,不需要额外添加空行
+        const nextHasComments = nextGroup && nextGroup.precedingComments && nextGroup.precedingComments.length > 0;
+
+        if (bothAreRelatedGroups) {
+            // 相关组内部不添加空行
+        } else if (!isLastGroup && !nextHasComments) {
+            text += '\n';  // 词组之间用空行分隔
+        } else if (isLastGroup) {
+            text += '\n';  // 最后一个词组后也保留一个空行
+        }
+    });
+
+    return text;
+}
+
+function syncFrequencyToUI() {
+    const data = parseFrequencyText(currentFrequency);
+    currentFrequencyData = data;
+    renderFrequencyPanel(data);
+}
+
+function renderFrequencyPanel(data) {
+    if (!data) {
+        data = parseFrequencyText(currentFrequency);
+    }
+
+    const panel = document.getElementById('frequency-panel');
+
+    // 辅助函数:根据关键词类型返回样式类
+    function getKeywordClass(keyword) {
+        if (keyword.startsWith('+')) return 'bg-green-500';
+        if (keyword.startsWith('!')) return 'bg-red-500';
+        if (keyword.startsWith('@')) return 'bg-purple-500';
+        if (keyword.startsWith('/') || keyword.includes('=>')) return 'bg-indigo-500';
+        return 'bg-blue-500';
+    }
+
+    // 辅助函数:为关键词添加标签
+    function getKeywordLabel(keyword) {
+        if (keyword.startsWith('+')) return '必须';
+        if (keyword.startsWith('!')) return '排除';
+        if (keyword.startsWith('@')) return '限制';
+        if (keyword.startsWith('/')) return '正则';
+        if (keyword.includes('=>')) return '别名';
+        return '';
+    }
+
+    // 渲染词组卡片
+    function renderGroupCard(group, idx) {
+        const jumpIcon = `<i class="fa-solid fa-grip-vertical text-gray-400 text-xs mr-2" title="拖动调整顺序"></i>`;
+
+        // 序号标记
+        const indexBadge = `<span class="text-xs bg-gray-700 text-white px-2.5 py-1 rounded-full font-bold mr-2" title="词组序号">#${idx + 1}</span>`;
+
+        // 相关组标记
+        const relatedGroupBadge = group.isRelatedGroup
+            ? `<span class="text-[10px] bg-gradient-to-r from-blue-500 to-indigo-500 text-white px-2 py-0.5 rounded font-bold ml-2" title="此组与相邻组相关(无空行分隔)">
+                <i class="fa-solid fa-link mr-1"></i>相关组 ${group.relatedGroupIndex + 1}/${group.relatedGroupTotal}
+               </span>`
+            : '';
+
+        // 相关组边框样式
+        const relatedGroupStyle = group.isRelatedGroup
+            ? 'border-l-4 border-l-blue-500 shadow-lg'
+            : '';
+
+        if (group.type === 'group-name') {
+            // 组别名类型
+            return `
+                <div class="word-group-card border-2 border-orange-200 bg-orange-50 group ${relatedGroupStyle} cursor-move" data-group-index="${idx}" onclick="scrollToWordGroupInEditor(${idx})">
+                    <div class="flex items-center justify-between mb-3">
+                        <div class="flex items-center flex-1 gap-2">
+                            ${jumpIcon}
+                            ${indexBadge}
+                            <span class="text-[10px] bg-orange-500 text-white px-2 py-0.5 rounded font-bold">组别名</span>
+                            ${relatedGroupBadge}
+                            <input type="text" value="${group.name || ''}" placeholder="组别名(如:东亚)"
+                                   class="text-sm font-bold border-0 border-b-2 border-orange-300 focus:border-orange-500 outline-none px-2 py-1 flex-1 bg-transparent"
+                                   onclick="event.stopPropagation()"
+                                   onchange="updateGroupName(${idx}, this.value)">
+                        </div>
+                        <button onclick="event.stopPropagation(); removeWordGroup(${idx})" class="text-red-500 hover:text-red-700 text-xs ml-2">
+                            <i class="fa-solid fa-trash"></i>
+                        </button>
+                    </div>
+                    <div class="bg-white rounded p-3 border border-orange-200 editable-area" onclick="event.stopPropagation()">
+                        <div class="text-xs text-gray-600 mb-2 font-bold">关键词列表:</div>
+                        <div class="tag-input-container">
+                            ${group.keywords.map(kw => {
+                                const label = getKeywordLabel(kw);
+                                const escapedKw = kw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+                                return `
+                                    <span class="tag-item ${getKeywordClass(kw)} relative break-all cursor-pointer" data-keyword="${escapedKw}" onclick="editKeyword(${idx}, this.dataset.keyword, this)">
+                                        ${label ? `<span class="text-[9px] opacity-75 mr-1">[${label}]</span>` : ''}
+                                        ${escapedKw}
+                                        <button data-keyword="${escapedKw}" onclick="event.stopPropagation(); removeKeyword(${idx}, this.dataset.keyword)">×</button>
+                                    </span>
+                                `;
+                            }).join('')}
+                            <input type="text" class="tag-input" placeholder="输入关键词后按回车..."
+                                   onkeydown="handleKeywordInput(event, ${idx})">
+                        </div>
+                        <div class="flex items-center justify-between mt-2">
+                            <button onclick="openDeepSeekAI('group', ${idx})" class="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
+                                <i class="fa-solid fa-wand-magic-sparkles"></i>AI 写正则
+                            </button>
+                            <div class="text-[10px] text-gray-400">${group.keywords.length} 个关键词</div>
+                        </div>
+                    </div>
+                </div>
+            `;
+        } else if (group.type === 'alias') {
+            // 单个别名类型
+            const item = group.items[0];
+            return `
+                <div class="word-group-card border-2 border-teal-200 bg-teal-50 group ${relatedGroupStyle} cursor-move" data-group-index="${idx}" onclick="scrollToWordGroupInEditor(${idx})">
+                    <div class="flex items-center justify-between mb-3">
+                        <div class="flex items-center flex-1 gap-2">
+                            ${jumpIcon}
+                            ${indexBadge}
+                            <span class="text-[10px] bg-teal-500 text-white px-2 py-0.5 rounded font-bold">单个别名</span>
+                            ${relatedGroupBadge}
+                        </div>
+                        <button onclick="event.stopPropagation(); removeWordGroup(${idx})" class="text-red-500 hover:text-red-700 text-xs">
+                            <i class="fa-solid fa-trash"></i>
+                        </button>
+                    </div>
+                    <div class="bg-white rounded p-3 border border-teal-200 editable-area" onclick="event.stopPropagation()">
+                        <div class="flex items-center gap-2">
+                            <input type="text" value="${item.keyword || ''}" placeholder="/正则/ 或 关键词"
+                                   class="flex-1 px-3 py-2 border border-gray-300 rounded focus:border-teal-500 outline-none text-sm font-mono"
+                                   onblur="updateAliasItem(${idx}, 0, 'keyword', this.value)">
+                            <span class="text-teal-600 font-bold">=></span>
+                            <input type="text" value="${item.alias || ''}" placeholder="别名"
+                                   class="flex-1 px-3 py-2 border border-gray-300 rounded focus:border-teal-500 outline-none text-sm"
+                                   onblur="updateAliasItem(${idx}, 0, 'alias', this.value)">
+                        </div>
+                        <div class="flex items-center justify-between mt-2">
+                            <button onclick="openDeepSeekAI('group', ${idx})" class="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
+                                <i class="fa-solid fa-wand-magic-sparkles"></i>AI 写正则
+                            </button>
+                            <div class="text-[10px] text-gray-500">
+                                <i class="fa-solid fa-lightbulb mr-1"></i>示例:/胖东来|于东来/ => 胖东来
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            `;
+        } else if (group.type === 'alias-group') {
+            // 连续别名组类型
+            return `
+                <div class="word-group-card border-2 border-purple-200 bg-purple-50 group ${relatedGroupStyle} cursor-move" data-group-index="${idx}" onclick="scrollToWordGroupInEditor(${idx})">
+                    <div class="flex items-center justify-between mb-3">
+                        <div class="flex items-center flex-1 gap-2">
+                            ${jumpIcon}
+                            ${indexBadge}
+                            <span class="text-[10px] bg-purple-500 text-white px-2 py-0.5 rounded font-bold">连续别名组</span>
+                            ${relatedGroupBadge}
+                        </div>
+                        <button onclick="event.stopPropagation(); removeWordGroup(${idx})" class="text-red-500 hover:text-red-700 text-xs">
+                            <i class="fa-solid fa-trash"></i>
+                        </button>
+                    </div>
+                    <div class="bg-white rounded p-3 border border-purple-200 space-y-2 editable-area" onclick="event.stopPropagation()">
+                        <div class="text-xs text-gray-600 mb-2 font-bold">
+                            别名列表(无空行分隔):
+                        </div>
+                        ${group.items.map((item, itemIdx) => `
+                            <div class="flex items-center gap-2">
+                                <input type="text" value="${item.keyword || ''}" placeholder="/正则/ 或 关键词"
+                                       class="flex-1 px-3 py-2 border border-gray-300 rounded focus:border-purple-500 outline-none text-sm font-mono"
+                                       onblur="updateAliasItem(${idx}, ${itemIdx}, 'keyword', this.value)">
+                                <span class="text-purple-600 font-bold">=></span>
+                                <input type="text" value="${item.alias || ''}" placeholder="别名"
+                                       class="flex-1 px-3 py-2 border border-gray-300 rounded focus:border-purple-500 outline-none text-sm"
+                                       onblur="updateAliasItem(${idx}, ${itemIdx}, 'alias', this.value)">
+                                <button onclick="removeAliasItem(${idx}, ${itemIdx})" class="text-red-500 hover:text-red-700 text-xs">
+                                    <i class="fa-solid fa-trash"></i>
+                                </button>
+                            </div>
+                        `).join('')}
+                        <div class="flex items-center justify-between mt-2">
+                            <button onclick="openDeepSeekAI('group', ${idx})" class="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
+                                <i class="fa-solid fa-wand-magic-sparkles"></i>AI 写正则
+                            </button>
+                            <div class="text-[10px] text-gray-500">
+                                <i class="fa-solid fa-info-circle mr-1"></i>这些别名行在配置文件中无空行分隔,属于同一组
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            `;
+        } else if (group.type === 'plain') {
+            // 普通词组类型
+            return `
+                <div class="word-group-card border-2 border-gray-200 bg-gray-50 group ${relatedGroupStyle} cursor-move" data-group-index="${idx}" onclick="scrollToWordGroupInEditor(${idx})">
+                    <div class="flex items-center justify-between mb-3">
+                        <div class="flex items-center flex-1 gap-2">
+                            ${jumpIcon}
+                            ${indexBadge}
+                            <span class="text-[10px] bg-gray-500 text-white px-2 py-0.5 rounded font-bold">普通词组</span>
+                            ${relatedGroupBadge}
+                        </div>
+                        <button onclick="event.stopPropagation(); removeWordGroup(${idx})" class="text-red-500 hover:text-red-700 text-xs">
+                            <i class="fa-solid fa-trash"></i>
+                        </button>
+                    </div>
+                    <div class="bg-white rounded p-3 border border-gray-200 editable-area" onclick="event.stopPropagation()">
+                        <div class="tag-input-container">
+                            ${group.keywords.map(kw => {
+                                const label = getKeywordLabel(kw);
+                                const escapedKw = kw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+                                return `
+                                    <span class="tag-item ${getKeywordClass(kw)} relative break-all cursor-pointer" data-keyword="${escapedKw}" onclick="editKeyword(${idx}, this.dataset.keyword, this)">
+                                        ${label ? `<span class="text-[9px] opacity-75 mr-1">[${label}]</span>` : ''}
+                                        ${escapedKw}
+                                        <button data-keyword="${escapedKw}" onclick="event.stopPropagation(); removeKeyword(${idx}, this.dataset.keyword)">×</button>
+                                    </span>
+                                `;
+                            }).join('')}
+                            <input type="text" class="tag-input" placeholder="输入关键词后按回车..."
+                                   onkeydown="handleKeywordInput(event, ${idx})">
+                        </div>
+                        <div class="flex items-center justify-between mt-2">
+                            <button onclick="openDeepSeekAI('group', ${idx})" class="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
+                                <i class="fa-solid fa-wand-magic-sparkles"></i>AI 写正则
+                            </button>
+                            <div class="text-[10px] text-gray-400">${group.keywords.length} 个关键词</div>
+                        </div>
+                    </div>
+                </div>
+            `;
+        }
+        return '';
+    }
+
+    panel.innerHTML = `
+        <!-- 规则说明区域 -->
+        <div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg border border-blue-200 p-4 mb-4">
+            <div class="flex items-start gap-3">
+                <i class="fa-solid fa-book text-blue-600 text-lg mt-0.5"></i>
+                <div class="flex-1">
+                    <h3 class="text-sm font-bold text-gray-800 mb-2">四种词组类型说明</h3>
+                    <div class="grid grid-cols-2 gap-3 text-xs">
+                        <div class="bg-white rounded p-2 border-l-4 border-orange-500">
+                            <div class="font-bold text-orange-700 mb-1">组别名</div>
+                            <div class="text-gray-600 font-mono text-[10px] mb-1">[东亚]<br>日本<br>韩国</div>
+                            <div class="text-gray-500 text-[10px]">多个关键词,统一显示为组名</div>
+                        </div>
+                        <div class="bg-white rounded p-2 border-l-4 border-teal-500">
+                            <div class="font-bold text-teal-700 mb-1">单个别名</div>
+                            <div class="text-gray-600 font-mono text-[10px] mb-1">/胖东来|于东来/ => 胖东来</div>
+                            <div class="text-gray-500 text-[10px]">正则匹配,显示为别名</div>
+                        </div>
+                        <div class="bg-white rounded p-2 border-l-4 border-purple-500">
+                            <div class="font-bold text-purple-700 mb-1">连续别名组</div>
+                            <div class="text-gray-600 font-mono text-[10px] mb-1">/智元|稚晖君/ => 智元<br>/众擎|EngineAI/ => 众擎</div>
+                            <div class="text-gray-500 text-[10px]">多个别名无空行分隔</div>
+                        </div>
+                        <div class="bg-white rounded p-2 border-l-4 border-gray-500">
+                            <div class="font-bold text-gray-700 mb-1">普通词组</div>
+                            <div class="text-gray-600 font-mono text-[10px] mb-1">申奥</div>
+                            <div class="text-gray-500 text-[10px]">普通关键词</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Global Filter 区域 -->
+        <div class="bg-white rounded-lg border border-gray-200 p-5">
+            <div class="flex items-center justify-between mb-3">
+                <h3 class="text-sm font-bold text-gray-700">
+                    <i class="fa-solid fa-filter mr-2"></i>全局过滤词
+                </h3>
+                <button onclick="openDeepSeekAI('global')" class="text-xs text-blue-600 hover:text-blue-700 flex items-center gap-1">
+                    <i class="fa-solid fa-wand-magic-sparkles"></i>AI 写正则
+                </button>
+            </div>
+            <div id="global-filter-tags" class="tag-input-container">
+                ${data.globalFilter.map(f => `
+                    <span class="tag-item ${getKeywordClass(f)}">
+                        ${f}
+                        <button onclick="removeGlobalFilter('${f.replace(/'/g, "\\'")}')">×</button>
+                    </span>
+                `).join('')}
+                <input type="text" class="tag-input" placeholder="输入过滤词后按回车..." onkeydown="handleGlobalFilterInput(event)">
+            </div>
+            <div class="text-xs text-gray-500 mt-2">
+                <i class="fa-solid fa-lightbulb mr-1"></i>提示:支持正则表达式(用 /.../ 包裹)
+            </div>
+        </div>
+
+        <!-- Word Groups 区域 -->
+        <div class="bg-white rounded-lg border border-gray-200 p-5">
+            <div class="flex items-center justify-between mb-3">
+                <h3 class="text-sm font-bold text-gray-700">
+                    <i class="fa-solid fa-layer-group mr-2"></i>关键词组 <span class="text-xs text-gray-400 font-normal">(${data.wordGroups.length} 个词组)</span>
+                </h3>
+                <button onclick="addWordGroup('top')" class="text-xs bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">
+                    <i class="fa-solid fa-plus mr-1"></i>添加词组
+                </button>
+            </div>
+            <div id="word-groups-container" class="space-y-3">
+                ${data.wordGroups.map((group, idx) => {
+                    const card = renderGroupCard(group, idx);
+                    // 在每个词组后添加插入区域(最后一个除外)
+                    if (idx < data.wordGroups.length - 1) {
+                        return card + `
+                            <div class="insert-zone group/insert" data-insert-index="${idx + 1}">
+                                <button onclick="insertWordGroupAt(${idx + 1})" class="insert-button">
+                                    <i class="fa-solid fa-plus"></i>
+                                </button>
+                            </div>
+                        `;
+                    }
+                    return card;
+                }).join('')}
+            </div>
+
+            <!-- 底部添加按钮 -->
+            <div class="mt-4 flex justify-center">
+                <button onclick="addWordGroup('bottom')" class="text-sm bg-gradient-to-r from-blue-500 to-blue-600 text-white px-6 py-2 rounded-lg hover:from-blue-600 hover:to-blue-700 shadow-sm transition-all flex items-center gap-2">
+                    <i class="fa-solid fa-plus-circle"></i>
+                    <span>在底部添加词组</span>
+                </button>
+            </div>
+        </div>
+    `;
+
+    // 初始化拖拽排序功能
+    setTimeout(() => {
+        const container = document.getElementById('word-groups-container');
+        if (container && typeof Sortable !== 'undefined') {
+            // 销毁之前的实例(如果存在)
+            if (container.sortableInstance) {
+                container.sortableInstance.destroy();
+            }
+
+            // 创建新的 Sortable 实例
+            container.sortableInstance = new Sortable(container, {
+                animation: 150,
+                filter: '.editable-area, input, button, select, textarea',  // 排除编辑区域
+                preventOnFilter: false,  // 允许在过滤区域正常交互
+                ghostClass: 'sortable-ghost',
+                chosenClass: 'sortable-chosen',
+                dragClass: 'sortable-drag',
+                onEnd: function(evt) {
+                    // 获取所有词组卡片的当前顺序
+                    const cards = Array.from(container.querySelectorAll('.word-group-card'));
+                    const newOrder = cards.map(card => parseInt(card.getAttribute('data-group-index')));
+
+                    // 检查顺序是否改变
+                    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+                    const oldOrder = data.wordGroups.map((_, idx) => idx);
+
+                    if (JSON.stringify(newOrder) !== JSON.stringify(oldOrder)) {
+                        // 根据新顺序重新排列数据
+                        const reorderedGroups = newOrder.map(idx => data.wordGroups[idx]);
+                        data.wordGroups = reorderedGroups;
+
+                        // 重新构建文本
+                        currentFrequency = buildFrequencyText(data);
+                        currentFrequencyData = parseFrequencyText(currentFrequency);
+                        document.getElementById('frequency-editor').value = currentFrequency;
+                        updateBackdrop('frequency-editor', 'frequency-backdrop');
+
+                        // 重新渲染
+                        renderFrequencyPanel(currentFrequencyData);
+                    }
+                }
+            });
+        }
+    }, 0);
+}
+
+// Global Filter 操作
+window.handleGlobalFilterInput = function(event) {
+    if (event.key === 'Enter' && event.target.value.trim()) {
+        const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+        data.globalFilter.push(event.target.value.trim());
+        currentFrequency = buildFrequencyText(data);
+        currentFrequencyData = data;
+        document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+        renderFrequencyPanel(data);
+    }
+}
+
+window.removeGlobalFilter = function(filter) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    data.globalFilter = data.globalFilter.filter(f => f !== filter);
+    currentFrequency = buildFrequencyText(data);
+    currentFrequencyData = data;
+    document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+    renderFrequencyPanel(data);
+}
+
+// Word Groups 操作
+let pendingWordGroupPosition = 'top';  // 记录添加位置:'top', 'bottom', 或数字索引
+
+window.addWordGroup = function(position = 'top') {
+    pendingWordGroupPosition = position;
+    document.getElementById('wordgroup-type-modal').classList.remove('hidden');
+}
+
+// 在指定位置插入词组
+window.insertWordGroupAt = function(index) {
+    pendingWordGroupPosition = index;  // 记录插入位置(数字索引)
+    document.getElementById('wordgroup-type-modal').classList.remove('hidden');
+}
+
+window.closeWordGroupTypeModal = function() {
+    document.getElementById('wordgroup-type-modal').classList.add('hidden');
+}
+
+window.confirmAddWordGroup = function(type) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    let newGroup;
+
+    if (type === 'group') {
+        // 组别名类型
+        newGroup = { type: 'group-name', name: '', keywords: [] };
+    } else if (type === 'alias') {
+        // 单个别名类型
+        newGroup = { type: 'alias', items: [{ keyword: '', alias: '' }] };
+    } else if (type === 'multi-alias') {
+        // 连续别名类型(多个别名行)
+        newGroup = { type: 'alias-group', items: [{ keyword: '', alias: '' }, { keyword: '', alias: '' }] };
+    } else if (type === 'plain') {
+        // 普通词组类型
+        newGroup = { type: 'plain', keywords: [] };
+    }
+
+    // 根据位置插入
+    if (pendingWordGroupPosition === 'bottom') {
+        data.wordGroups.push(newGroup);
+    } else if (pendingWordGroupPosition === 'top') {
+        data.wordGroups.unshift(newGroup);
+    } else if (typeof pendingWordGroupPosition === 'number') {
+        // 在指定索引位置插入
+        data.wordGroups.splice(pendingWordGroupPosition, 0, newGroup);
+    }
+
+    currentFrequency = buildFrequencyText(data);
+    currentFrequencyData = data;
+    document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+    renderFrequencyPanel(data);
+
+    closeWordGroupTypeModal();
+
+    // 滚动到新添加的词组
+    setTimeout(() => {
+        const container = document.getElementById('word-groups-container');
+        if (pendingWordGroupPosition === 'bottom') {
+            container.scrollTop = container.scrollHeight;
+        } else if (pendingWordGroupPosition === 'top') {
+            container.scrollTop = 0;
+        } else if (typeof pendingWordGroupPosition === 'number') {
+            // 滚动到插入的位置
+            const cards = container.querySelectorAll('.word-group-card');
+            if (cards[pendingWordGroupPosition]) {
+                cards[pendingWordGroupPosition].scrollIntoView({ behavior: 'smooth', block: 'center' });
+            }
+        }
+    }, 100);
+}
+
+window.removeWordGroup = function(index) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    data.wordGroups.splice(index, 1);
+    currentFrequency = buildFrequencyText(data);
+    // 重新解析以更新相关组信息
+    currentFrequencyData = parseFrequencyText(currentFrequency);
+    document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+    renderFrequencyPanel(currentFrequencyData);
+}
+
+window.updateGroupName = function(index, name) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    const group = data.wordGroups[index];
+
+    // 只有 group-name 类型才有 name 字段
+    if (group.type === 'group-name') {
+        group.name = name;
+    }
+
+    currentFrequency = buildFrequencyText(data);
+    // 重新解析以更新相关组信息
+    currentFrequencyData = parseFrequencyText(currentFrequency);
+    document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+    renderFrequencyPanel(currentFrequencyData);
+}
+
+window.editKeyword = function(groupIndex, oldKeyword, spanElement) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    const group = data.wordGroups[groupIndex];
+
+    // 只有 group-name 和 plain 类型才有 keywords 字段
+    if (group.type !== 'group-name' && group.type !== 'plain') {
+        return;
+    }
+
+    const originalKeyword = group.keywords.find(kw => kw === oldKeyword) || oldKeyword;
+
+    const input = document.createElement('input');
+    input.type = 'text';
+    input.value = originalKeyword;
+    input.className = 'tag-input inline-block px-2 py-1 text-xs border border-blue-500 rounded';
+    input.style.minWidth = '100px';
+
+    const saveEdit = () => {
+        const newKeyword = input.value.trim();
+        if (newKeyword && newKeyword !== originalKeyword) {
+            const kwIndex = group.keywords.indexOf(originalKeyword);
+            if (kwIndex !== -1) {
+                group.keywords[kwIndex] = newKeyword;
+            }
+            currentFrequency = buildFrequencyText(data);
+            // 重新解析以更新相关组信息
+            currentFrequencyData = parseFrequencyText(currentFrequency);
+            document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+            renderFrequencyPanel(currentFrequencyData);
+        } else {
+            spanElement.style.display = '';
+            input.remove();
+        }
+    };
+
+    input.onblur = saveEdit;
+    input.onkeydown = (e) => {
+        if (e.key === 'Enter') {
+            saveEdit();
+        } else if (e.key === 'Escape') {
+            spanElement.style.display = '';
+            input.remove();
+        }
+    };
+
+    spanElement.style.display = 'none';
+    spanElement.parentNode.insertBefore(input, spanElement);
+    input.focus();
+    input.select();
+}
+
+window.handleKeywordInput = function(event, groupIndex) {
+    if (event.key === 'Enter' && event.target.value.trim()) {
+        const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+        const group = data.wordGroups[groupIndex];
+
+        // 只有 group-name 和 plain 类型才能添加关键词
+        if (group.type === 'group-name' || group.type === 'plain') {
+            group.keywords.push(event.target.value.trim());
+            event.target.value = '';
+
+            currentFrequency = buildFrequencyText(data);
+            // 重新解析以更新相关组信息
+            currentFrequencyData = parseFrequencyText(currentFrequency);
+            document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+            renderFrequencyPanel(currentFrequencyData);
+        }
+    }
+}
+
+window.removeKeyword = function(groupIndex, keyword) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    const group = data.wordGroups[groupIndex];
+
+    // 只有 group-name 和 plain 类型才能删除关键词
+    if (group.type === 'group-name' || group.type === 'plain') {
+        group.keywords = group.keywords.filter(k => k !== keyword);
+
+        // 如果词组变空,删除整个词组
+        if (group.keywords.length === 0) {
+            data.wordGroups.splice(groupIndex, 1);
+        }
+
+        currentFrequency = buildFrequencyText(data);
+        // 重新解析以更新相关组信息
+        currentFrequencyData = parseFrequencyText(currentFrequency);
+        document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+        renderFrequencyPanel(currentFrequencyData);
+    }
+}
+
+// 更新别名项
+window.updateAliasItem = function(groupIndex, itemIndex, field, value) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    const group = data.wordGroups[groupIndex];
+
+    // 只有 alias 和 alias-group 类型才有 items 字段
+    if (group.type === 'alias' || group.type === 'alias-group') {
+        if (group.items[itemIndex]) {
+            group.items[itemIndex][field] = value;
+
+            currentFrequency = buildFrequencyText(data);
+            currentFrequencyData = parseFrequencyText(currentFrequency);
+            document.getElementById('frequency-editor').value = currentFrequency;
+            updateBackdrop('frequency-editor', 'frequency-backdrop');
+            renderFrequencyPanel(currentFrequencyData);
+        }
+    }
+}
+
+// 添加别名项
+window.addAliasItem = function(groupIndex) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    const group = data.wordGroups[groupIndex];
+
+    // 只有 alias-group 类型才能添加别名项
+    if (group.type === 'alias-group') {
+        group.items.push({ keyword: '', alias: '' });
+
+        currentFrequency = buildFrequencyText(data);
+        // 重新解析以更新相关组信息
+        currentFrequencyData = parseFrequencyText(currentFrequency);
+        document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+        renderFrequencyPanel(currentFrequencyData);
+    } else if (group.type === 'alias') {
+        // 如果是单个别名,升级为别名组
+        group.type = 'alias-group';
+        group.items.push({ keyword: '', alias: '' });
+
+        currentFrequency = buildFrequencyText(data);
+        // 重新解析以更新相关组信息
+        currentFrequencyData = parseFrequencyText(currentFrequency);
+        document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+        renderFrequencyPanel(currentFrequencyData);
+    }
+}
+
+// 删除别名项
+window.removeAliasItem = function(groupIndex, itemIndex) {
+    const data = currentFrequencyData || parseFrequencyText(currentFrequency);
+    const group = data.wordGroups[groupIndex];
+
+    // 只有 alias-group 类型才能删除别名项
+    if (group.type === 'alias-group') {
+        group.items.splice(itemIndex, 1);
+
+        // 如果没有别名项了,删除整个词组
+        if (group.items.length === 0) {
+            data.wordGroups.splice(groupIndex, 1);
+        }
+        // 如果只剩一个别名项,降级为单个别名
+        else if (group.items.length === 1) {
+            group.type = 'alias';
+        }
+
+        currentFrequency = buildFrequencyText(data);
+        currentFrequencyData = parseFrequencyText(currentFrequency);
+        document.getElementById('frequency-editor').value = currentFrequency;
+    updateBackdrop('frequency-editor', 'frequency-backdrop');
+        renderFrequencyPanel(currentFrequencyData);
+    }
+}
+
+// DeepSeek AI 辅助
+window.openDeepSeekAI = function(type, groupIndex) {
+    const userInput = window.prompt('请输入核心关键词(例如:华为):');
+    if (!userInput) return;
+
+    const promptText = `我正在配置一个新闻聚合系统,需要通过 Python 正则表达式 抓取关于【${userInput}】的新闻。
+
+请帮我完成以下步骤,并最终只输出一个正则表达式字符串:
+
+第一步:【精准关键词筛选】
+请列出与【${userInput}】强绑定的核心词汇:
+1. 核心品牌:包括中文全称、简称、股票代码、别名。
+2. 核心人物:仅限最高决策层或极具代表性的创始人。
+3. 独家产品:必须是具有极高辨识度的独家产品名。
+4. 核心工作室/子品牌:强相关的下属机构。
+
+第二步:【严格清洗与过滤】(请严格执行)
+1. 包含关系去重(最短匹配原则):
+   - 中文:如果列表里已经有了核心短词(如“腾讯”),请删除所有包含该短词的长词(如“腾讯云”、“腾讯视频”统统不要,因为它们会被短词命中)。
+   - 英文:如果有了 \\bKeyword\\b,就不要再出现 Keyword。
+2. 彻底排除无关公司:
+   - 绝对不要包含:该品牌的竞争对手、合作伙伴(如京东、美团、字节跳动等非隶属公司)。
+3. 彻底排除通用黑话:
+   - 绝对不要包含:行业通用词(如“互联网”、“大厂”、“新质生产力”、“人工智能”、“元宇宙”、“金融科技”等)。
+
+第三步:【构建 Python 正则】
+将清洗后的词汇合并,格式要求如下:
+1. 英文处理:所有英文单词必须前后加 \\b(例如 \\bWord\\b),严禁出现没有边界符的英文单词。
+2. 连接符:用 | 连接。
+
+最终输出示例格式:
+/词A|词B|\\bEnglishWord\\b/ => ${userInput}
+
+输出要求:
+- 只要这一行正则表达式,不要任何解释,不要代码块。`;
+
+    const textArea = document.createElement("textarea");
+    textArea.value = promptText;
+
+    textArea.style.position = "fixed";
+    textArea.style.left = "-9999px";
+    textArea.style.top = "0";
+    document.body.appendChild(textArea);
+
+    textArea.focus();
+    textArea.select();
+
+    let copySuccess = false;
+    try {
+        copySuccess = document.execCommand('copy');
+    } catch (err) {
+        console.error('复制失败:', err);
+    }
+
+    document.body.removeChild(textArea);
+
+    if (copySuccess) {
+        if (confirm(`提示词已复制到剪贴板!\n\n关键词:${userInput}\n\n点击【确定】跳转 DeepSeek 官网,直接粘贴 (Ctrl+V) 即可。`)) {
+            window.open('https://chat.deepseek.com/', '_blank');
+        }
+    } else {
+        prompt('自动复制失败,请手动复制以下内容,然后自行打开 DeepSeek:', promptText);
+        window.open('https://chat.deepseek.com/', '_blank');
+    }
+}
+
+// ==========================================
+// 10. 平台管理功能
+// ==========================================
+
+// 解析当前配置中的平台列表
+function parsePlatformsFromYaml() {
+    try {
+        const doc = jsyaml.load(currentYaml);
+        if (doc && doc.platforms && doc.platforms.sources) {
+            return doc.platforms.sources;
+        }
+    } catch (e) {}
+    return [];
+}
+
+// 渲染平台列表
+function renderPlatformsList() {
+    const container = document.getElementById('platforms-list');
+    if (!container) return;
+
+    const platforms = parsePlatformsFromYaml();
+
+    if (platforms.length === 0) {
+        container.innerHTML = `<div class="text-xs text-gray-400 italic">暂无平台,请添加</div>`;
+        return;
+    }
+
+    container.innerHTML = platforms.map((p, idx) => `
+        <div class="platform-item flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2 border border-gray-200 hover:border-blue-300 transition-colors" data-index="${idx}">
+            <div class="flex items-center gap-2">
+                <i class="fa-solid fa-grip-vertical text-gray-300 cursor-move"></i>
+                <span class="text-xs font-medium text-gray-700">${p.name}</span>
+                <span class="text-[10px] text-gray-400">(${p.id})</span>
+            </div>
+            <button onclick="removePlatform(${idx})" class="text-red-400 hover:text-red-600 text-xs" title="删除">
+                <i class="fa-solid fa-trash"></i>
+            </button>
+        </div>
+    `).join('');
+
+    // 初始化拖拽排序
+    if (typeof Sortable !== 'undefined') {
+        new Sortable(container, {
+            animation: 150,
+            handle: '.fa-grip-vertical',
+            onEnd: function(evt) {
+                reorderPlatforms(evt.oldIndex, evt.newIndex);
+            }
+        });
+    }
+}
+
+// 删除平台
+window.removePlatform = function(index) {
+    const platforms = parsePlatformsFromYaml();
+    if (index < 0 || index >= platforms.length) return;
+
+    const platformName = platforms[index].name;
+    if (!confirm(`确定要删除平台 "${platformName}" 吗?`)) return;
+
+    platforms.splice(index, 1);
+    updatePlatformsInYaml(platforms);
+}
+
+// 重新排序平台
+function reorderPlatforms(oldIndex, newIndex) {
+    const platforms = parsePlatformsFromYaml();
+    const [removed] = platforms.splice(oldIndex, 1);
+    platforms.splice(newIndex, 0, removed);
+    updatePlatformsInYaml(platforms);
+}
+
+// 更新 YAML 中的平台配置(保留注释)
+function updatePlatformsInYaml(platforms) {
+    const editor = document.getElementById('yaml-editor');
+    let yaml = editor.value;
+    const lines = yaml.split('\n');
+
+    // 找到 platforms.sources 的位置
+    let sourcesStart = -1;
+    let sourcesEnd = -1;
+    let inPlatforms = false;
+    let inSources = false;
+    let baseIndent = 0;
+    let lastDataLineIndex = -1; // 记录最后一个数据行的位置
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        const trimmed = line.trim();
+
+        if (line.match(/^platforms:/)) {
+            inPlatforms = true;
+            continue;
+        }
+
+        if (inPlatforms && !inSources && trimmed.startsWith('sources:')) {
+            sourcesStart = i + 1;
+            inSources = true;
+            baseIndent = line.search(/\S/) + 2; // sources 下一级的缩进
+            continue;
+        }
+
+        if (inSources) {
+            const currentIndent = line.search(/\S/);
+
+            // 如果是数据行(以 - 开头或是数据项的属性)
+            if (trimmed.startsWith('-')) {
+                lastDataLineIndex = i;
+            } else if (trimmed && !trimmed.startsWith('#') && currentIndent >= baseIndent) {
+                // 数据项的属性行(如 name:, id:)
+                lastDataLineIndex = i;
+            } else if (trimmed && !trimmed.startsWith('#') && currentIndent < baseIndent) {
+                // 遇到缩进更小的非注释行,说明离开了 sources 区域
+                sourcesEnd = lastDataLineIndex + 1;
+                break;
+            }
+        }
+
+        // 检查是否进入下一个顶级模块
+        if (inPlatforms && line.match(/^[a-z_]+:/) && !line.match(/^platforms:/)) {
+            if (lastDataLineIndex >= 0) {
+                sourcesEnd = lastDataLineIndex + 1;
+            } else {
+                sourcesEnd = i;
+            }
+            break;
+        }
+    }
+
+    // 如果没有找到结束位置,使用最后一个数据行的下一行
+    if (sourcesEnd === -1) {
+        sourcesEnd = lastDataLineIndex >= 0 ? lastDataLineIndex + 1 : lines.length;
+    }
+
+    // 提取区域内的注释(保留在开头的注释)
+    const regionLines = lines.slice(sourcesStart, sourcesEnd);
+    const leadingComments = [];
+    for (const line of regionLines) {
+        const trimmed = line.trim();
+        if (trimmed.startsWith('#')) {
+            leadingComments.push(line);
+        } else if (trimmed.startsWith('-') || (trimmed && !trimmed.startsWith('#'))) {
+            // 遇到第一个数据项,停止收集注释
+            break;
+        } else if (trimmed === '') {
+            // 空行也保留
+            leadingComments.push(line);
+        }
+    }
+
+    const indent = '    '; // 4 空格缩进
+    const newSourcesLines = platforms.map(p =>
+        `${indent}- id: "${p.id}"\n${indent}  name: "${p.name}"`
+    ).join('\n');
+
+    const beforeSources = lines.slice(0, sourcesStart);
+    const afterSources = lines.slice(sourcesEnd);
+
+    // 组合:前面内容 + 开头注释 + 新数据 + 后面内容
+    const newYaml = [
+        ...beforeSources,
+        ...(leadingComments.length > 0 ? leadingComments : []),
+        newSourcesLines,
+        ...afterSources
+    ].join('\n');
+
+    editor.value = newYaml;
+    currentYaml = newYaml;
+    updateBackdrop('yaml-editor', 'yaml-backdrop');
+    debounceSaveConfig();
+    renderPlatformsList();
+    renderStandaloneLists(); // 同步更新独立展示区的平台选择列表
+}
+
+// ==========================================
+// 12. Display Regions 排序与管理功能
+// ==========================================
+
+const DISPLAY_REGIONS_DEF = [
+    { key: "hotlist", label: "热榜区域" },
+    { key: "new_items", label: "新增热点区域" },
+    { key: "rss", label: "RSS 订阅区域" },
+    { key: "standalone", label: "独立展示区" },
+    { key: "ai_analysis", label: "AI 分析区域" }
+];
+
+// 从 YAML 解析 display.regions,严格按照 region_order 定义顺序
+function parseDisplayRegionsFromYaml() {
+    try {
+        const doc = jsyaml.load(currentYaml);
+        if (doc && doc.display) {
+            const regionOrder = doc.display.region_order || [];
+            const regionStates = doc.display.regions || {};
+
+            // 严格按 region_order 顺序构建列表
+            if (regionOrder.length > 0) {
+                return regionOrder.map(key => {
+                    const normalizedKey = key === 'new_item' ? 'new_items' : key;
+                    const def = DISPLAY_REGIONS_DEF.find(d => d.key === normalizedKey);
+                    return {
+                        key: normalizedKey,
+                        label: def ? def.label : normalizedKey,
+                        enabled: regionStates[normalizedKey] !== undefined ? regionStates[normalizedKey] : false
+                    };
+                });
+            }
+
+            // 后备方案:如果没有 region_order,使用 regions 对象的顺序
+            const regions = [];
+            for (const key in regionStates) {
+                const normalizedKey = key === 'new_item' ? 'new_items' : key;
+                const def = DISPLAY_REGIONS_DEF.find(d => d.key === normalizedKey);
+                if (def) {
+                    regions.push({
+                        key: normalizedKey,
+                        label: def.label,
+                        enabled: regionStates[key]
+                    });
+                }
+            }
+            return regions;
+        }
+    } catch (e) {}
+
+    // 默认返回所有区域(禁用状态)
+    return DISPLAY_REGIONS_DEF.map(def => ({
+        key: def.key,
+        label: def.label,
+        enabled: false
+    }));
+}
+
+// 渲染 Display Regions 列表
+function renderDisplayRegionsList() {
+    const container = document.getElementById('display-regions-list');
+    if (!container) return;
+
+    const regions = parseDisplayRegionsFromYaml();
+
+    container.innerHTML = regions.map((r, idx) => `
+        <div class="display-region-item flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2 border border-gray-200 hover:border-blue-300 transition-colors" data-key="${r.key}">
+            <div class="flex items-center gap-2">
+                <i class="fa-solid fa-grip-vertical text-gray-300 cursor-move"></i>
+                <span class="text-xs font-medium ${r.enabled ? 'text-gray-700' : 'text-gray-400'}">${r.label}</span>
+                <span class="text-[10px] text-gray-400">(${r.key})</span>
+            </div>
+            <div class="relative inline-block w-10 align-middle select-none">
+                <input type="checkbox" id="toggle-region-${r.key}"
+                       ${r.enabled ? 'checked' : ''}
+                       onchange="toggleDisplayRegion('${r.key}')"
+                       class="toggle-checkbox absolute block w-4 h-4 mt-0.5 ml-0.5 rounded-full bg-white border-4 appearance-none cursor-pointer transition-all duration-200 ease-in-out"/>
+                <label for="toggle-region-${r.key}" class="toggle-label block overflow-hidden h-5 rounded-full bg-gray-300 cursor-pointer"></label>
+            </div>
+        </div>
+    `).join('');
+
+    // 初始化拖拽排序
+    if (typeof Sortable !== 'undefined') {
+        new Sortable(container, {
+            animation: 150,
+            handle: '.fa-grip-vertical',
+            onEnd: function(evt) {
+                reorderDisplayRegions();
+            }
+        });
+    }
+}
+
+// 切换区域启用状态
+window.toggleDisplayRegion = function(key) {
+    const regions = parseDisplayRegionsFromYaml();
+    const target = regions.find(r => r.key === key);
+    if (target) {
+        target.enabled = !target.enabled;
+        updateDisplayRegionsInYaml(regions);
+    }
+}
+
+// 重新排序区域
+window.reorderDisplayRegions = function() {
+    const container = document.getElementById('display-regions-list');
+    const items = container.querySelectorAll('.display-region-item');
+    const newOrderKeys = Array.from(items).map(item => item.dataset.key);
+
+    const currentRegions = parseDisplayRegionsFromYaml();
+
+    const newRegions = newOrderKeys.map(key => {
+        return currentRegions.find(r => r.key === key);
+    }).filter(r => r); // 过滤掉可能的 undefined
+
+    updateDisplayRegionsInYaml(newRegions);
+}
+
+// 更新 YAML 中的 display.regions 和 display.region_order
+function updateDisplayRegionsInYaml(regions) {
+    const editor = document.getElementById('yaml-editor');
+    let yaml = editor.value;
+    const lines = yaml.split('\n');
+
+    let regionOrderStart = -1;
+    let regionOrderEnd = -1;
+    let regionsStart = -1;
+    let regionsEnd = -1;
+    let inDisplay = false;
+    let regionOrderIndent = 0;
+    let regionsIndent = 0;
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        const trimmed = line.trim();
+
+        if (line.match(/^display:/)) {
+            inDisplay = true;
+            continue;
+        }
+
+        if (!inDisplay) continue;
+
+        // 查找 region_order 数组
+        if (trimmed.startsWith('region_order:')) {
+            regionOrderStart = i + 1;
+            regionOrderIndent = line.search(/\S/) + 2;
+            // 找到 region_order 的结束位置
+            for (let j = i + 1; j < lines.length; j++) {
+                const nextLine = lines[j];
+                const nextTrimmed = nextLine.trim();
+                if (nextTrimmed && !nextTrimmed.startsWith('#') && !nextTrimmed.startsWith('-')) {
+                    const nextIndent = nextLine.search(/\S/);
+                    if (nextIndent < regionOrderIndent) {
+                        regionOrderEnd = j;
+                        break;
+                    }
+                }
+            }
+            if (regionOrderEnd === -1) regionOrderEnd = lines.length;
+            continue;
+        }
+
+        // 查找 regions 对象
+        if (trimmed.startsWith('regions:')) {
+            regionsStart = i + 1;
+            regionsIndent = line.search(/\S/) + 2;
+            // 找到 regions 的结束位置(遇到同级或更高级的键)
+            for (let j = i + 1; j < lines.length; j++) {
+                const nextLine = lines[j];
+                const nextTrimmed = nextLine.trim();
+                if (nextTrimmed && !nextTrimmed.startsWith('#')) {
+                    const nextIndent = nextLine.search(/\S/);
+                    // 检查是否是同级或更高级的键(如 standalone:)
+                    if (nextIndent <= line.search(/\S/)) {
+                        regionsEnd = j;
+                        break;
+                    }
+                }
+            }
+            if (regionsEnd === -1) regionsEnd = lines.length;
+            break;
+        }
+
+        // 检查是否离开 display 模块
+        if (line.match(/^[a-z_]+:/) && !line.match(/^display:/)) {
+            break;
+        }
+    }
+
+    // 更新 region_order 数组(保留注释)
+    if (regionOrderStart > 0 && regionOrderEnd > regionOrderStart) {
+        const indentStr = ' '.repeat(regionOrderIndent);
+
+        // 提取原有行的注释映射
+        const originalRegionOrderBlock = lines.slice(regionOrderStart, regionOrderEnd);
+        const commentMap = {};
+
+        originalRegionOrderBlock.forEach(line => {
+            // 匹配 "- key  # 注释" 格式
+            const match = line.match(/^\s*-\s*([a-z_]+)\s*(#.*)?$/);
+            if (match) {
+                const key = match[1];
+                const comment = match[2] || '';
+                if (key) commentMap[key] = comment;
+            }
+        });
+
+        // 生成新的行,保留注释
+        const newRegionOrderLines = regions.map(r => {
+            const comment = commentMap[r.key] || '';
+            return `${indentStr}- ${r.key}${comment ? '                       ' + comment : ''}`;
+        });
+
+        lines.splice(regionOrderStart, regionOrderEnd - regionOrderStart, ...newRegionOrderLines);
+
+        // 调整 regionsStart 和 regionsEnd
+        const lineDiff = newRegionOrderLines.length - (regionOrderEnd - regionOrderStart);
+        if (regionsStart > regionOrderEnd) {
+            regionsStart += lineDiff;
+            regionsEnd += lineDiff;
+        }
+    }
+
+    // 更新 regions 对象
+    if (regionsStart > 0 && regionsEnd > regionsStart) {
+        const originalRegionsBlock = lines.slice(regionsStart, regionsEnd);
+        const commentMap = {};
+
+        originalRegionsBlock.forEach(line => {
+            const match = line.match(/^\s*([a-z_]+):\s*[^#]*(#.*)?$/);
+            if (match) {
+                const key = match[1];
+                const comment = match[2] || '';
+                if (key) commentMap[key] = comment;
+            }
+        });
+
+        const indentStr = ' '.repeat(regionsIndent);
+        const newRegionsLines = regions.map(r => {
+            const comment = commentMap[r.key] || '';
+            return `${indentStr}${r.key}: ${r.enabled}${comment ? ' ' + comment.trim() : ''}`;
+        });
+
+        lines.splice(regionsStart, regionsEnd - regionsStart, ...newRegionsLines);
+    }
+
+    editor.value = lines.join('\n');
+    currentYaml = lines.join('\n');
+    updateBackdrop('yaml-editor', 'yaml-backdrop');
+    debounceSaveConfig();
+
+    renderDisplayRegionsList();
+}
+
+// 解析当前配置中的 RSS 源列表
+function parseRssFeedsFromYaml() {
+    try {
+        const doc = jsyaml.load(currentYaml);
+        if (doc && doc.rss && doc.rss.feeds) {
+            return doc.rss.feeds;
+        }
+    } catch (e) {}
+    return [];
+}
+
+// 渲染 RSS 源列表
+function renderRssFeedsList() {
+    const container = document.getElementById('rss-feeds-list');
+    if (!container) return;
+
+    const feeds = parseRssFeedsFromYaml();
+
+    if (feeds.length === 0) {
+        container.innerHTML = `<div class="text-xs text-gray-400 italic">暂无 RSS 源,请添加</div>`;
+        return;
+    }
+
+    container.innerHTML = feeds.map((f, idx) => `
+        <div class="rss-feed-item bg-gray-50 rounded-lg px-3 py-2 border border-gray-200 hover:border-blue-300 transition-colors" data-index="${idx}">
+            <div class="flex items-center justify-between">
+                <div class="flex items-center gap-2 flex-1 min-w-0">
+                    <i class="fa-solid fa-rss text-orange-400"></i>
+                    <span class="text-xs font-medium text-gray-700 truncate">${f.name}</span>
+                    <span class="text-[10px] text-gray-400">(${f.id})</span>
+                    ${f.enabled === false ? '<span class="text-[9px] bg-gray-200 text-gray-500 px-1 rounded">已禁用</span>' : ''}
+                </div>
+                <div class="flex items-center gap-1">
+                    <button onclick="editRssFeed(${idx})" class="text-blue-400 hover:text-blue-600 text-xs px-1" title="编辑">
+                        <i class="fa-solid fa-pen"></i>
+                    </button>
+                    <button onclick="toggleRssFeed(${idx})" class="text-gray-400 hover:text-gray-600 text-xs px-1" title="${f.enabled === false ? '启用' : '禁用'}">
+                        <i class="fa-solid fa-${f.enabled === false ? 'eye' : 'eye-slash'}"></i>
+                    </button>
+                    <button onclick="removeRssFeed(${idx})" class="text-red-400 hover:text-red-600 text-xs px-1" title="删除">
+                        <i class="fa-solid fa-trash"></i>
+                    </button>
+                </div>
+            </div>
+            <div class="text-[10px] text-gray-400 mt-1 truncate" title="${f.url}">${f.url}</div>
+        </div>
+    `).join('');
+}
+
+// 删除 RSS 源
+window.removeRssFeed = function(index) {
+    const feeds = parseRssFeedsFromYaml();
+    if (index < 0 || index >= feeds.length) return;
+
+    const feedName = feeds[index].name;
+    if (!confirm(`确定要删除 RSS 源 "${feedName}" 吗?`)) return;
+
+    feeds.splice(index, 1);
+    updateRssFeedsInYaml(feeds);
+}
+
+// 切换 RSS 源启用状态
+window.toggleRssFeed = function(index) {
+    const feeds = parseRssFeedsFromYaml();
+    if (index < 0 || index >= feeds.length) return;
+
+    feeds[index].enabled = feeds[index].enabled === false ? true : false;
+    updateRssFeedsInYaml(feeds);
+}
+
+// 编辑 RSS 源
+window.editRssFeed = function(index) {
+    const feeds = parseRssFeedsFromYaml();
+    if (index < 0 || index >= feeds.length) return;
+
+    const feed = feeds[index];
+
+    openRssModalWithData(feed, index);
+}
+
+// 更新 YAML 中的 RSS 配置(保留注释)
+function updateRssFeedsInYaml(feeds) {
+    const editor = document.getElementById('yaml-editor');
+    let yaml = editor.value;
+    const lines = yaml.split('\n');
+
+    // 找到 rss.feeds 的位置
+    let feedsStart = -1;
+    let feedsEnd = -1;
+    let inRss = false;
+    let inFeeds = false;
+    let lastDataLineIndex = -1; // 记录最后一个数据行的位置
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        const trimmed = line.trim();
+
+        if (line.match(/^rss:/)) {
+            inRss = true;
+            continue;
+        }
+
+        if (inRss && !inFeeds && trimmed.startsWith('feeds:')) {
+            feedsStart = i + 1;
+            inFeeds = true;
+            continue;
+        }
+
+        if (inFeeds) {
+            const indent = line.search(/\S/);
+
+            // 如果是数据行(以 - 开头或是数据项的属性)
+            if (trimmed.startsWith('-')) {
+                lastDataLineIndex = i;
+            } else if (trimmed && !trimmed.startsWith('#') && indent > 2) {
+                // 数据项的属性行(如 name:, id:, url:)
+                lastDataLineIndex = i;
+            } else if (trimmed && !trimmed.startsWith('#') && indent <= 2 && indent >= 0) {
+                // 遇到缩进更小的非注释行,说明离开了 feeds 区域
+                feedsEnd = lastDataLineIndex + 1;
+                break;
+            }
+        }
+
+        // 检查是否进入下一个顶级模块
+        if (inRss && line.match(/^[a-z_]+:/) && !line.match(/^rss:/)) {
+            if (lastDataLineIndex >= 0) {
+                feedsEnd = lastDataLineIndex + 1;
+            } else {
+                feedsEnd = i;
+            }
+            break;
+        }
+    }
+
+    // 如果没有找到结束位置,使用最后一个数据行的下一行
+    if (feedsEnd === -1) {
+        feedsEnd = lastDataLineIndex >= 0 ? lastDataLineIndex + 1 : lines.length;
+    }
+
+    // 提取区域内的注释(保留在开头的注释)
+    const regionLines = lines.slice(feedsStart, feedsEnd);
+    const leadingComments = [];
+    for (const line of regionLines) {
+        const trimmed = line.trim();
+        if (trimmed.startsWith('#')) {
+            leadingComments.push(line);
+        } else if (trimmed.startsWith('-') || (trimmed && !trimmed.startsWith('#'))) {
+            // 遇到第一个数据项,停止收集注释
+            break;
+        } else if (trimmed === '') {
+            // 空行也保留
+            leadingComments.push(line);
+        }
+    }
+
+    // 构建新的 feeds 内容
+    const indent = '    '; // 4 空格缩进
+    const newFeedsLines = feeds.map(f => {
+        let feedYaml = `${indent}- id: "${f.id}"\n${indent}  name: "${f.name}"\n${indent}  url: "${f.url}"`;
+        if (f.enabled === false) {
+            feedYaml += `\n${indent}  enabled: false`;
+        }
+        if (f.max_age_days !== undefined && f.max_age_days !== '') {
+            feedYaml += `\n${indent}  max_age_days: ${f.max_age_days}`;
+        }
+        return feedYaml;
+    }).join('\n\n');
+
+    const beforeFeeds = lines.slice(0, feedsStart);
+    const afterFeeds = lines.slice(feedsEnd);
+
+    // 组合:前面内容 + 开头注释 + 新数据 + 空行 + 后面内容
+    const newYaml = [
+        ...beforeFeeds,
+        ...(leadingComments.length > 0 ? leadingComments : []),
+        newFeedsLines,
+        '',
+        ...afterFeeds
+    ].join('\n');
+
+    editor.value = newYaml;
+    currentYaml = newYaml;
+    updateBackdrop('yaml-editor', 'yaml-backdrop');
+    debounceSaveConfig();
+    renderRssFeedsList();
+    renderStandaloneLists(); // 同步更新独立展示区的 RSS 选择列表
+}
+
+// 打开 RSS 添加/编辑弹窗
+window.openRssModal = function() {
+    openRssModalWithData(null, -1);
+}
+
+function openRssModalWithData(feed, editIndex) {
+    const modal = document.getElementById('rss-modal');
+
+    document.getElementById('rss-id').value = feed ? feed.id : '';
+    document.getElementById('rss-name').value = feed ? feed.name : '';
+    document.getElementById('rss-url').value = feed ? feed.url : '';
+    document.getElementById('rss-max-age').value = feed && feed.max_age_days !== undefined ? feed.max_age_days : '';
+
+    modal.dataset.editIndex = editIndex;
+
+    const title = modal.querySelector('h3');
+    if (title) {
+        title.innerHTML = editIndex >= 0 ?
+            '<i class="fa-solid fa-rss mr-2 text-orange-500"></i>编辑 RSS 源' :
+            '<i class="fa-solid fa-rss mr-2 text-orange-500"></i>添加 RSS 源';
+    }
+
+    modal.classList.remove('hidden');
+}
+
+// 关闭 RSS 弹窗
+window.closeRssModal = function() {
+    const modal = document.getElementById('rss-modal');
+    modal.classList.add('hidden');
+    modal.dataset.editIndex = '-1';
+
+    document.getElementById('rss-id').value = '';
+    document.getElementById('rss-name').value = '';
+    document.getElementById('rss-url').value = '';
+    document.getElementById('rss-max-age').value = '';
+}
+
+// 确认添加/编辑 RSS
+window.confirmAddRss = function() {
+    const modal = document.getElementById('rss-modal');
+    const editIndex = parseInt(modal.dataset.editIndex || '-1');
+
+    const id = document.getElementById('rss-id').value.trim();
+    const name = document.getElementById('rss-name').value.trim();
+    const url = document.getElementById('rss-url').value.trim();
+    const maxAge = document.getElementById('rss-max-age').value.trim();
+
+    if (!id || !name || !url) {
+        alert('请填写完整信息:ID、名称和 URL 都是必填项');
+        return;
+    }
+
+    const feeds = parseRssFeedsFromYaml();
+
+    const newFeed = { id, name, url };
+    if (maxAge) {
+        newFeed.max_age_days = parseInt(maxAge);
+    }
+
+    if (editIndex >= 0) {
+        feeds[editIndex] = newFeed;
+    } else {
+        feeds.push(newFeed);
+    }
+
+    updateRssFeedsInYaml(feeds);
+    closeRssModal();
+}
+
+// ==========================================
+// 14. 独立展示区 (Standalone) 管理功能
+// ==========================================
+
+function parseStandaloneConfigFromYaml() {
+    try {
+        const doc = jsyaml.load(currentYaml);
+        if (doc && doc.display && doc.display.standalone) {
+            return {
+                platforms: doc.display.standalone.platforms || [],
+                rss_feeds: doc.display.standalone.rss_feeds || []
+            };
+        }
+    } catch (e) {}
+    return { platforms: [], rss_feeds: [] };
+}
+
+function renderStandaloneLists() {
+    const platformsContainer = document.getElementById('standalone-platforms-list');
+    const rssContainer = document.getElementById('standalone-rss-list');
+
+    if (!platformsContainer || !rssContainer) return;
+
+    const standaloneConfig = parseStandaloneConfigFromYaml();
+    const availablePlatforms = parsePlatformsFromYaml();
+    const availableRss = parseRssFeedsFromYaml();
+
+    // Render Platforms
+    if (availablePlatforms.length === 0) {
+        platformsContainer.innerHTML = `<div class="col-span-2 text-xs text-gray-400 italic">暂无可用平台</div>`;
+    } else {
+        platformsContainer.innerHTML = availablePlatforms.map(p => {
+            const isChecked = standaloneConfig.platforms.includes(p.id);
+            return `
+                <label class="flex items-center gap-2 p-1.5 rounded hover:bg-white transition-colors cursor-pointer">
+                    <input type="checkbox" onchange="toggleStandaloneItem('platforms', '${p.id}')"
+                           ${isChecked ? 'checked' : ''} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
+                    <div class="min-w-0">
+                        <div class="text-xs font-medium text-gray-700 truncate">${p.name}</div>
+                        <div class="text-[9px] text-gray-400 truncate">${p.id}</div>
+                    </div>
+                </label>
+            `;
+        }).join('');
+    }
+
+    // Render RSS
+    if (availableRss.length === 0) {
+        rssContainer.innerHTML = `<div class="text-xs text-gray-400 italic">暂无可用 RSS 源</div>`;
+    } else {
+        rssContainer.innerHTML = availableRss.map(f => {
+            const isChecked = standaloneConfig.rss_feeds.includes(f.id);
+            return `
+                <label class="flex items-center gap-2 p-1.5 rounded hover:bg-white transition-colors cursor-pointer">
+                    <input type="checkbox" onchange="toggleStandaloneItem('rss_feeds', '${f.id}')"
+                           ${isChecked ? 'checked' : ''} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
+                    <div class="min-w-0 flex-1">
+                        <div class="flex items-center justify-between">
+                            <span class="text-xs font-medium text-gray-700 truncate">${f.name}</span>
+                            <span class="text-[9px] text-gray-400 ml-2">${f.id}</span>
+                        </div>
+                        <div class="text-[9px] text-gray-400 truncate">${f.url}</div>
+                    </div>
+                </label>
+            `;
+        }).join('');
+    }
+}
+
+window.toggleStandaloneItem = function(type, id) {
+    const config = parseStandaloneConfigFromYaml();
+    const list = config[type];
+
+    const index = list.indexOf(id);
+    if (index === -1) {
+        list.push(id);
+    } else {
+        list.splice(index, 1);
+    }
+
+    updateStandaloneConfigInYaml(type, list);
+}
+
+function updateStandaloneConfigInYaml(type, list) {
+    const editor = document.getElementById('yaml-editor');
+    let yaml = editor.value;
+    const lines = yaml.split('\n');
+
+    // 找到 display -> standalone -> [type]
+    let inDisplay = false;
+    let inStandalone = false;
+    let targetLineIndex = -1;
+    let indent = '';
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        if (line.match(/^display:/)) {
+            inDisplay = true;
+            continue;
+        }
+        if (inDisplay && line.trim().startsWith('standalone:')) {
+            inStandalone = true;
+            continue;
+        }
+        if (inStandalone) {
+            // 检查是否离开 standalone (遇到缩进更少或相同的非注释行)
+            const currentIndent = line.search(/\S/);
+            // standalone 下一级的缩进
+            if (line.match(new RegExp(`^\\s*${type}:`))) {
+                targetLineIndex = i;
+                indent = line.substring(0, line.indexOf(type));
+                break;
+            }
+            // 如果遇到下一个模块,停止
+            if (line.match(/^[a-z_]+:/) && !line.match(/^display:/)) break;
+        }
+    }
+
+    if (targetLineIndex !== -1) {
+        // 构建新的数组字符串 ["item1", "item2"]
+        const jsonStr = JSON.stringify(list);
+        // 保留原有注释
+        const originalLine = lines[targetLineIndex];
+        const commentMatch = originalLine.match(/#.*$/);
+        const comment = commentMatch ? commentMatch[0] : '';
+
+        lines[targetLineIndex] = `${indent}${type}: ${jsonStr}${comment ? ' ' + comment : ''}`;
+
+        const newYaml = lines.join('\n');
+        editor.value = newYaml;
+        currentYaml = newYaml;
+        updateBackdrop('yaml-editor', 'yaml-backdrop');
+        debounceSaveConfig();
+
+        // 不需要重新渲染整个列表,因为是 checkbox 点击触发的
+        // 但如果需要保持一致性,可以重新渲染
+    }
+}
+
+
+// 从文本中提取版本号
+function extractVersion(text) {
+    // 匹配 Version: v5.3.0 或 Version: 5.3.0 格式
+    const versionMatch = text.match(/Version:\s*v?(\d+\.\d+\.\d+)/i);
+    if (versionMatch) {
+        return versionMatch[1]; // 返回不带 v 的版本号
+    }
+    return null;
+}
+
+// 比较版本号 (返回 1: v1 > v2, -1: v1 < v2, 0: v1 == v2)
+function compareVersions(v1, v2) {
+    if (!v1 || !v2) return 0;
+
+    const parts1 = v1.split('.').map(Number);
+    const parts2 = v2.split('.').map(Number);
+
+    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
+        const num1 = parts1[i] || 0;
+        const num2 = parts2[i] || 0;
+
+        if (num1 > num2) return 1;
+        if (num1 < num2) return -1;
+    }
+
+    return 0;
+}
+
+// 版本检测主函数
+window.checkVersion = async function() {
+    const btn = document.getElementById('version-check-btn');
+    const originalHTML = btn.innerHTML;
+
+    btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i><span>检测中...</span>';
+    btn.disabled = true;
+
+    try {
+        const versionRes = await fetch(REMOTE_VERSION_URL);
+        if (!versionRes.ok) {
+            throw new Error(`版本信息获取失败: ${versionRes.status}`);
+        }
+
+        const versionConfigText = await versionRes.text();
+        const versionMap = {};
+        versionConfigText.split('\n').forEach(line => {
+            const parts = line.trim().split('=');
+            if (parts.length >= 2) {
+                versionMap[parts[0].trim()] = parts[1].trim();
+            }
+        });
+
+        const currentTab = getCurrentTab();
+        let currentVersion = null;
+        let fileName = '';
+
+        if (currentTab === 'config') {
+            currentVersion = extractVersion(currentYaml);
+            fileName = 'config.yaml';
+        } else {
+            currentVersion = extractVersion(currentFrequency);
+            fileName = 'frequency_words.txt';
+        }
+
+        const latestVersion = versionMap[fileName];
+
+        if (!latestVersion) {
+             throw new Error(`未在远程版本清单中找到 ${fileName}`);
+        }
+
+        showVersionComparisonModal(fileName, currentVersion, latestVersion);
+
+    } catch (err) {
+        console.error('版本检测失败:', err);
+        showToast(`版本检测失败: ${err.message}`, 'error');
+    } finally {
+        btn.innerHTML = originalHTML;
+        btn.disabled = false;
+    }
+}
+
+// 获取当前 Tab
+function getCurrentTab() {
+    return currentTab; 
+}
+
+// 显示版本对比弹窗
+function showVersionComparisonModal(fileName, currentVersion, latestVersion) {
+    const existingModal = document.getElementById('version-comparison-modal');
+    if (existingModal) existingModal.remove();
+
+    const comparison = compareVersions(currentVersion, latestVersion);
+    let statusIcon = '';
+    let statusText = '';
+    let statusColor = '';
+    let actionButtons = '';
+
+    if (!currentVersion) {
+        statusIcon = '<i class="fa-solid fa-question-circle text-gray-500 text-3xl"></i>';
+        statusText = '未检测到版本信息';
+        statusColor = 'text-gray-600';
+        actionButtons = `
+            <button onclick="closeVersionModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">关闭</button>
+            <button onclick="updateToLatest()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
+                <i class="fa-solid fa-download mr-1"></i>更新到最新版本
+            </button>
+        `;
+    } else if (comparison < 0) {
+        statusIcon = '<i class="fa-solid fa-arrow-up text-orange-500 text-3xl"></i>';
+        statusText = '发现新版本';
+        statusColor = 'text-orange-600';
+        actionButtons = `
+            <button onclick="closeVersionModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">稍后更新</button>
+            <button onclick="updateToLatest()" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700">
+                <i class="fa-solid fa-download mr-1"></i>立即更新
+            </button>
+        `;
+    } else if (comparison > 0) {
+        statusIcon = '<i class="fa-solid fa-flask text-purple-500 text-3xl"></i>';
+        statusText = '当前版本较新(开发版本?)';
+        statusColor = 'text-purple-600';
+        actionButtons = `
+            <button onclick="closeVersionModal()" class="px-4 py-2 bg-gray-100 text-gray-600 hover:bg-gray-200 rounded-lg">关闭</button>
+        `;
+    } else {
+        statusIcon = '<i class="fa-solid fa-check-circle text-green-500 text-3xl"></i>';
+        statusText = '已是最新版本';
+        statusColor = 'text-green-600';
+        actionButtons = `
+            <button onclick="closeVersionModal()" class="px-4 py-2 bg-gray-100 text-gray-600 hover:bg-gray-200 rounded-lg">关闭</button>
+        `;
+    }
+
+    const modal = document.createElement('div');
+    modal.id = 'version-comparison-modal';
+    modal.className = 'modal-overlay';
+    modal.innerHTML = `
+        <div class="modal-content" style="max-width: 480px;">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-bold text-gray-800">
+                    <i class="fa-solid fa-code-compare mr-2 text-blue-500"></i>版本检测结果
+                </h3>
+                <button onclick="closeVersionModal()" class="text-gray-400 hover:text-gray-600">
+                    <i class="fa-solid fa-times text-xl"></i>
+                </button>
+            </div>
+
+            <div class="text-center py-6">
+                ${statusIcon}
+                <div class="text-xl font-bold ${statusColor} mt-3">${statusText}</div>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4 space-y-3 mb-4">
+                <div class="flex items-center justify-between text-sm">
+                    <span class="text-gray-600">配置文件</span>
+                    <span class="font-mono font-bold text-gray-800">${fileName}</span>
+                </div>
+                <div class="border-t border-gray-200"></div>
+                <div class="flex items-center justify-between text-sm">
+                    <span class="text-gray-600">当前版本</span>
+                    <span class="font-mono font-bold ${currentVersion ? 'text-blue-600' : 'text-gray-400'}">
+                        ${currentVersion ? 'v' + currentVersion : '未知'}
+                    </span>
+                </div>
+                <div class="flex items-center justify-between text-sm">
+                    <span class="text-gray-600">最新版本</span>
+                    <span class="font-mono font-bold text-green-600">v${latestVersion}</span>
+                </div>
+            </div>
+
+            ${comparison < 0 || !currentVersion ? `
+                <div class="text-xs text-gray-500 bg-yellow-50 border border-yellow-200 rounded p-3 mb-4">
+                    <i class="fa-solid fa-lightbulb mr-1 text-yellow-600"></i>
+                    <strong>提示:</strong>更新将从 GitHub 加载最新的 ${fileName},你当前的修改将被覆盖。建议先复制保存你的自定义配置。
+                </div>
+            ` : ''}
+
+            <div class="flex justify-end gap-2">
+                ${actionButtons}
+            </div>
+        </div>
+    `;
+
+    document.body.appendChild(modal);
+}
+
+window.closeVersionModal = function() {
+    const modal = document.getElementById('version-comparison-modal');
+    if (modal) modal.remove();
+}
+
+// ==========================================
+// 13. 平台添加弹窗逻辑
+// ==========================================
+
+// 预定义可用平台列表 (仅包含官方默认支持的平台)
+const PRESET_PLATFORMS = [
+    { key: 'toutiao', name: '今日头条' },
+    { key: 'baidu', name: '百度热搜' },
+    { key: 'wallstreetcn-hot', name: '华尔街见闻' },
+    { key: 'thepaper', name: '澎湃新闻' },
+    { key: 'bilibili-hot-search', name: 'bilibili 热搜' },
+    { key: 'cls-hot', name: '财联社热门' },
+    { key: 'ifeng', name: '凤凰网' },
+    { key: 'tieba', name: '贴吧' },
+    { key: 'weibo', name: '微博' },
+    { key: 'douyin', name: '抖音' },
+    { key: 'zhihu', name: '知乎' }
+];
+
+/**
+ * 打开平台添加弹窗
+ */
+window.openPlatformModal = function() {
+    const modal = document.getElementById('platform-modal');
+    if (modal) {
+        modal.classList.remove('hidden');
+        if (typeof switchPlatformTab === 'function') {
+            switchPlatformTab('select');
+        }
+        renderAvailablePlatforms();
+    }
+}
+
+/**
+ * 关闭平台添加弹窗
+ */
+window.closePlatformModal = function() {
+    const modal = document.getElementById('platform-modal');
+    if (modal) {
+        modal.classList.add('hidden');
+    }
+}
+
+/**
+ * 切换平台添加标签页
+ */
+window.switchPlatformTab = function(tab) {
+    currentPlatformTab = tab;
+
+    // 更新 Tab 样式
+    const tabSelect = document.getElementById('tab-platform-select');
+    const tabCustom = document.getElementById('tab-platform-custom');
+
+    if (tab === 'select') {
+        if (tabSelect) {
+            tabSelect.classList.add('text-blue-600', 'border-blue-600');
+            tabSelect.classList.remove('text-gray-500', 'border-transparent');
+        }
+        if (tabCustom) {
+            tabCustom.classList.remove('text-blue-600', 'border-blue-600');
+            tabCustom.classList.add('text-gray-500', 'border-transparent');
+        }
+
+        const selectPanel = document.getElementById('platform-select-panel');
+        const customPanel = document.getElementById('platform-custom-panel');
+        if (selectPanel) selectPanel.classList.remove('hidden');
+        if (customPanel) customPanel.classList.add('hidden');
+    } else {
+        if (tabCustom) {
+            tabCustom.classList.add('text-blue-600', 'border-blue-600');
+            tabCustom.classList.remove('text-gray-500', 'border-transparent');
+        }
+        if (tabSelect) {
+            tabSelect.classList.remove('text-blue-600', 'border-blue-600');
+            tabSelect.classList.add('text-gray-500', 'border-transparent');
+        }
+
+        const selectPanel = document.getElementById('platform-select-panel');
+        const customPanel = document.getElementById('platform-custom-panel');
+        if (selectPanel) selectPanel.classList.add('hidden');
+        if (customPanel) customPanel.classList.remove('hidden');
+    }
+}
+
+/**
+ * 渲染可用平台列表(排除已添加的)
+ */
+function renderAvailablePlatforms() {
+    const container = document.getElementById('available-platforms-list');
+    const tip = document.getElementById('no-platforms-tip');
+    if (!container) return;
+    container.innerHTML = '';
+
+    const currentPlatforms = parsePlatformsFromYaml();
+    const existingKeys = currentPlatforms.map(p => p.id); 
+
+    const available = PRESET_PLATFORMS.filter(p => !existingKeys.includes(p.key));
+
+    if (available.length === 0) {
+        if (tip) {
+            tip.classList.remove('hidden');
+            tip.innerHTML = `<i class="fa-solid fa-check-circle text-green-500 mr-2"></i>所有预设平台已添加`;
+        }
+    } else {
+        if (tip) tip.classList.add('hidden');
+
+        available.forEach(p => {
+            const div = document.createElement('div');
+            div.className = 'flex items-center justify-between p-3 border border-gray-100 rounded hover:bg-blue-50 cursor-pointer transition-colors group';
+            div.onclick = () => confirmAddPlatform(p.key, p.name);
+            div.innerHTML = `
+                <div class="flex items-center gap-3">
+                    <div class="w-8 h-8 rounded bg-gray-100 flex items-center justify-center text-gray-500 group-hover:bg-white group-hover:text-blue-600">
+                        <i class="fa-solid fa-cube"></i>
+                    </div>
+                    <div>
+                        <div class="font-bold text-gray-800 text-sm">${p.name}</div>
+                        <div class="text-xs text-gray-400 font-mono">${p.key}</div>
+                    </div>
+                </div>
+                <button class="text-gray-300 group-hover:text-blue-600">
+                    <i class="fa-solid fa-plus-circle text-lg"></i>
+                </button>
+            `;
+            container.appendChild(div);
+        });
+    }
+}
+
+/**
+ * 确认添加平台
+ */
+window.confirmAddPlatform = function(key, name) {
+    let platformKey = key;
+    let platformName = name;
+
+    // 如果是手动输入模式 (且未传入 key)
+    if (currentPlatformTab === 'custom' && !key) {
+        const keyInput = document.getElementById('custom-platform-key');
+        const nameInput = document.getElementById('custom-platform-name');
+
+        if (keyInput) platformKey = keyInput.value.trim();
+        if (nameInput) platformName = nameInput.value.trim();
+
+        if (!platformKey) {
+            alert('请输入平台 Key');
+            return;
+        }
+        if (!platformName) {
+            platformName = platformKey;
+        }
+    } else if (currentPlatformTab === 'select' && !key) {
+        alert('请直接点击上方列表中的平台进行添加');
+        return;
+    }
+
+    // 检查是否已存在
+    const currentPlatforms = parsePlatformsFromYaml();
+    if (currentPlatforms.find(p => p.id === platformKey)) {
+        alert(`平台 ${platformKey} 已存在!`);
+        return;
+    }
+
+    // 添加到 YAML (注意字段是 id 和 name)
+    const newPlatform = {
+        id: platformKey,
+        name: platformName,
+        enabled: true
+    };
+
+    // 重新构建 YAML
+    currentPlatforms.push(newPlatform);
+    updatePlatformsInYaml(currentPlatforms);
+
+    closePlatformModal();
+
+    const keyInput = document.getElementById('custom-platform-key');
+    const nameInput = document.getElementById('custom-platform-name');
+    if (keyInput) keyInput.value = '';
+    if (nameInput) nameInput.value = '';
+
+    renderPlatformsList();
+
+    showToast(`平台 ${platformName} 已添加`, 'success');
+}
+
+// 绑定到全局
+window.updateToLatest = async function() {
+    closeVersionModal();
+
+    const currentTab = getCurrentTab();
+    const fileName = currentTab === 'config' ? 'config.yaml' : 'frequency_words.txt';
+
+    if (!confirm(`确定要从 GitHub 更新 ${fileName} 到最新版本吗?\n\n你当前的自定义配置将被覆盖,建议先复制保存。`)) {
+        return;
+    }
+
+    showToast('正在从 GitHub 加载最新版本...', 'info');
+
+    try {
+        const url = currentTab === 'config' ? REMOTE_CONFIG_URL : REMOTE_FREQUENCY_URL;
+        const res = await fetch(url);
+
+        if (!res.ok) {
+            throw new Error(`加载失败: ${res.status}`);
+        }
+
+        const text = await res.text();
+
+        if (currentTab === 'config') {
+            try {
+                jsyaml.load(text);
+            } catch (yamlErr) {
+                showToast(`YAML 语法错误: ${yamlErr.message}`, 'error');
+                return;
+            }
+            document.getElementById('yaml-editor').value = text;
+            currentYaml = text;
+            syncYamlToUI();
+        } else {
+            document.getElementById('frequency-editor').value = text;
+            currentFrequency = text;
+            syncFrequencyToUI();
+        }
+
+        saveToLocalStorage();
+
+        showToast(`已更新到最新版本`, 'success');
+
+    } catch (err) {
+        console.error('更新失败:', err);
+        showToast(`更新失败: ${err.message}`, 'error');
+    }
+}
+
+// ==========================================
+// RSS 辅助功能
+// ==========================================
+
+function toggleRssTips() {
+    const panel = document.getElementById('rss-tips-panel');
+    const icon = document.getElementById('rss-tips-icon');
+    if (panel) {
+        panel.classList.toggle('hidden');
+        if (icon) {
+            icon.style.transform = panel.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)';
+        }
+    }
+}
+
+function fillRssUrl(url) {
+    const input = document.getElementById('rss-url');
+    if (input) {
+        input.value = url;
+        // 视觉反馈
+        input.classList.add('ring-2', 'ring-blue-500', 'bg-blue-50');
+        setTimeout(() => {
+            input.classList.remove('ring-2', 'ring-blue-500', 'bg-blue-50');
+        }, 500);
+    }
+}

+ 560 - 0
docs/assets/style.css

@@ -0,0 +1,560 @@
+/* 编辑器区域滚动条 */
+#yaml-editor::-webkit-scrollbar,
+#frequency-editor::-webkit-scrollbar,
+#yaml-backdrop::-webkit-scrollbar,
+#frequency-backdrop::-webkit-scrollbar {
+    width: 10px;
+    height: 10px;
+}
+#yaml-editor::-webkit-scrollbar-track,
+#frequency-editor::-webkit-scrollbar-track,
+#yaml-backdrop::-webkit-scrollbar-track,
+#frequency-backdrop::-webkit-scrollbar-track {
+    background: #1e1e1e;
+}
+#yaml-editor::-webkit-scrollbar-thumb,
+#frequency-editor::-webkit-scrollbar-thumb,
+#yaml-backdrop::-webkit-scrollbar-thumb,
+#frequency-backdrop::-webkit-scrollbar-thumb {
+    background: #424242;
+    border-radius: 0;
+}
+#yaml-editor::-webkit-scrollbar-thumb:hover,
+#frequency-editor::-webkit-scrollbar-thumb:hover,
+#yaml-backdrop::-webkit-scrollbar-thumb:hover,
+#frequency-backdrop::-webkit-scrollbar-thumb:hover {
+    background: #4f4f4f;
+}
+
+/* 高亮编辑器容器 */
+.highlight-editor-wrap {
+    position: relative;
+    flex: 1;
+    display: flex;
+    overflow: hidden;
+}
+
+/* 高亮背景层 */
+.highlight-backdrop {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    padding: 1rem;
+    margin: 0;
+    border: none;
+    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+    font-size: 0.75rem;
+    line-height: 1.625;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    overflow: auto;
+    background: #1e1e1e;
+    color: #d4d4d4;
+    pointer-events: none;
+    z-index: 1;
+}
+
+/* 透明输入层 */
+.highlight-textarea {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    padding: 1rem;
+    margin: 0;
+    border: none;
+    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+    font-size: 0.75rem;
+    line-height: 1.625;
+    overflow: auto;
+    background: transparent;
+    color: transparent;
+    caret-color: #d4d4d4;
+    resize: none;
+    outline: none;
+    z-index: 2;
+}
+
+/* 注释样式 - 灰色 */
+.syntax-comment {
+    color: #6a9955;
+}
+
+/* 右侧面板滚动条 */
+#modules-container::-webkit-scrollbar {
+    width: 8px;
+}
+#modules-container::-webkit-scrollbar-track {
+    background: transparent;
+}
+#modules-container::-webkit-scrollbar-thumb {
+    background: #cbd5e1;
+    border-radius: 4px;
+}
+
+/* 模块卡片样式 */
+.module-card {
+    background: white;
+    border-radius: 0.5rem; /* rounded-lg */
+    border: 1px solid #e5e7eb; /* border-gray-200 */
+    overflow: hidden;
+    transition: all 0.2s;
+}
+
+/* 激活态(可编辑) */
+.module-card.active {
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+.module-card.active:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+    border-color: #bfdbfe; /* blue-200 */
+}
+.module-card.active .module-header {
+    background-color: #fff;
+    border-bottom: 1px solid #f3f4f6;
+    color: #111827;
+}
+
+/* 禁用态(灰色/只读) */
+.module-card.disabled {
+    background-color: #f9fafb; /* gray-50 */
+    opacity: 0.8;
+}
+.module-card.disabled .module-header {
+    background-color: #f3f4f6; /* gray-100 */
+    color: #6b7280; /* gray-500 */
+    cursor: not-allowed;
+}
+.module-card.disabled .module-body {
+    display: none;
+}
+.module-card.disabled .locked-badge {
+    display: inline-flex;
+}
+
+/* 输入控件统一 */
+input[type="text"],
+input[type="password"],
+input[type="number"],
+select {
+    font-size: 0.875rem; /* text-sm */
+    line-height: 1.25rem;
+    padding: 0.5rem 0.75rem;
+    border-radius: 0.375rem;
+    border-width: 1px;
+    border-color: #d1d5db; /* gray-300 */
+    width: 100%;
+    outline: 2px solid transparent;
+    transition: all 0.15s;
+}
+input:focus, select:focus {
+    border-color: #3b82f6; /* blue-500 */
+    box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+}
+
+/* 开关样式 (Checkbox Toggle) */
+.toggle-checkbox:checked {
+    right: 0;
+    border-color: #3b82f6;
+}
+.toggle-checkbox:checked + .toggle-label {
+    background-color: #3b82f6;
+}
+
+/* 列表样式 (Platforms & RSS & Sortable) */
+.sortable-list-item {
+    background: #f8fafc;
+    border: 1px solid #e2e8f0;
+    margin-bottom: 0.5rem;
+    border-radius: 0.375rem;
+    transition: all 0.2s;
+}
+.sortable-list-item:hover {
+    border-color: #cbd5e1;
+    background: #f1f5f9;
+}
+.sortable-handle {
+    cursor: grab;
+    color: #94a3b8;
+}
+.sortable-handle:hover {
+    color: #64748b;
+}
+.sortable-ghost {
+    background: #e2e8f0;
+    opacity: 0.5;
+}
+
+/* 禁用状态的勾选框 */
+input[type="checkbox"]:disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+}
+
+/* Tab 切换样式 */
+.tab-button {
+    transition: all 0.2s;
+}
+.tab-button.active {
+    color: #d4d4d4;
+    border-color: #3b82f6;
+}
+.tab-content.hidden {
+    display: none;
+}
+
+/* 标签输入样式 */
+.tag-input-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+    padding: 0.5rem;
+    border: 1px solid #d1d5db;
+    border-radius: 0.375rem;
+    background: white;
+    min-height: 42px;
+}
+.tag-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 0.25rem;
+    padding: 0.25rem 0.5rem;
+    background: #3b82f6;
+    color: white;
+    border-radius: 0.25rem;
+    font-size: 0.875rem;
+}
+.tag-item button {
+    background: none;
+    border: none;
+    color: white;
+    cursor: pointer;
+    padding: 0;
+    font-size: 1rem;
+    line-height: 1;
+}
+.tag-input {
+    flex: 1;
+    border: none;
+    outline: none;
+    min-width: 120px;
+    font-size: 0.875rem;
+}
+
+/* 词组卡片样式 */
+.word-group-card {
+    background: white;
+    border: 1px solid #e5e7eb;
+    border-radius: 0.5rem;
+    padding: 1rem;
+    transition: all 0.2s;
+}
+.word-group-card:hover {
+    border-color: #3b82f6;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+/* 插入区域样式 */
+.insert-zone {
+    position: relative;
+    height: 8px;
+    margin: 0.5rem 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+}
+
+.insert-zone:hover {
+    height: 32px;
+}
+
+.insert-button {
+    opacity: 0;
+    visibility: hidden;
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #3b82f6, #2563eb);
+    color: white;
+    border: 2px solid white;
+    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s;
+    font-size: 14px;
+}
+
+.insert-zone:hover .insert-button {
+    opacity: 1;
+    visibility: visible;
+}
+
+.insert-button:hover {
+    transform: scale(1.1);
+    box-shadow: 0 4px 12px rgba(59, 130, 246, 0.6);
+    background: linear-gradient(135deg, #2563eb, #1d4ed8);
+}
+
+.insert-button:active {
+    transform: scale(0.95);
+}
+
+/* 编辑区域恢复默认鼠标样式 */
+.word-group-card .editable-area {
+    cursor: default;
+}
+.word-group-card .editable-area input {
+    cursor: text;
+}
+.word-group-card .editable-area button {
+    cursor: pointer;
+}
+.word-group-card .editable-area .tag-item {
+    cursor: pointer;
+}
+
+/* 拖拽手柄样式 */
+.drag-handle {
+    cursor: grab;
+    transition: all 0.2s;
+}
+.drag-handle:active {
+    cursor: grabbing;
+}
+
+/* SortableJS 拖拽样式 */
+.sortable-ghost {
+    opacity: 0.4;
+    background: #dbeafe;
+    border: 2px dashed #3b82f6;
+}
+.sortable-chosen {
+    background: #f0f9ff;
+    border-color: #3b82f6;
+}
+.sortable-drag {
+    opacity: 0.8;
+    box-shadow: 0 10px 20px rgba(0,0,0,0.2);
+    transform: rotate(2deg);
+}
+
+/* 独立区域复选框组 */
+.checkbox-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+    gap: 0.75rem;
+}
+.checkbox-card {
+    display: flex;
+    align-items: center;
+    padding: 0.5rem;
+    border: 1px solid #e5e7eb;
+    border-radius: 0.375rem;
+    background-color: #fff;
+    cursor: pointer;
+    transition: all 0.15s;
+}
+.checkbox-card:hover {
+    border-color: #93c5fd;
+    background-color: #eff6ff;
+}
+.checkbox-card input:checked + span {
+    color: #2563eb;
+    font-weight: 500;
+}
+
+/* ==========================================
+   拖拽上传遮罩层
+   ========================================== */
+.drop-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(59, 130, 246, 0.9);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 100;
+    pointer-events: all;
+}
+.drop-overlay.hidden {
+    display: none;
+}
+.drop-overlay-content {
+    text-align: center;
+    color: white;
+}
+.drop-overlay-content i {
+    font-size: 3rem;
+    margin-bottom: 0.5rem;
+    animation: bounce 1s infinite;
+}
+@keyframes bounce {
+    0%, 100% { transform: translateY(0); }
+    50% { transform: translateY(-10px); }
+}
+
+/* ==========================================
+   Toast 提示
+   ========================================== */
+.toast-notification {
+    position: fixed;
+    bottom: 24px;
+    right: 24px;
+    display: flex;
+    align-items: center;
+    gap: 0.75rem;
+    padding: 0.875rem 1.25rem;
+    border-radius: 0.5rem;
+    font-size: 0.875rem;
+    font-weight: 500;
+    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+    z-index: 9999;
+    opacity: 0;
+    transform: translateY(20px);
+    transition: all 0.3s ease;
+}
+.toast-notification.show {
+    opacity: 1;
+    transform: translateY(0);
+}
+.toast-notification i {
+    font-size: 1.125rem;
+}
+
+/* Toast 类型样式 */
+.toast-success {
+    background: #10b981;
+    color: white;
+}
+.toast-error {
+    background: #ef4444;
+    color: white;
+}
+.toast-info {
+    background: #3b82f6;
+    color: white;
+}
+.toast-warning {
+    background: #f59e0b;
+    color: white;
+}
+
+/* ==========================================
+   弹窗样式
+   ========================================== */
+.modal-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 1000;
+}
+.modal-overlay.hidden {
+    display: none;
+}
+.modal-content {
+    background: white;
+    border-radius: 0.75rem;
+    padding: 1.5rem;
+    max-width: 450px;
+    width: 90%;
+    max-height: 90vh;
+    overflow-y: auto;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+
+/* 弹簧跳动动画 */
+@keyframes spring-in {
+    0% { transform: scale(0.5); opacity: 0; }
+    60% { transform: scale(1.1); }
+    80% { transform: scale(0.95); }
+    100% { transform: scale(1); opacity: 1; }
+}
+
+.support-modal-content {
+    animation: spring-in 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+    background: #ffffff;
+    border: none;
+    border-radius: 1.5rem;
+}
+
+/* 柔软卡片设计 */
+.support-card {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 1.5rem;
+    background: #fdfdfd;
+    border: 1px solid #f3f4f6;
+    border-radius: 1.25rem;
+    transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+    text-decoration: none;
+    cursor: pointer;
+    overflow: hidden;
+}
+
+.support-card:hover {
+    transform: translateY(-8px) scale(1.02);
+    background: white;
+    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
+    border-color: #e5e7eb;
+}
+
+.support-card-num {
+    position: absolute;
+    top: 1rem;
+    right: 1.25rem;
+    font-size: 0.75rem;
+    font-weight: 800;
+    color: #f3f4f6;
+    font-style: italic;
+    transition: color 0.3s;
+}
+
+.support-card:hover .support-card-num {
+    color: #e5e7eb;
+}
+
+.support-icon {
+    width: 3.5rem;
+    height: 3.5rem;
+    border-radius: 1rem;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.5rem;
+    margin-bottom: 1rem;
+    transition: all 0.3s ease;
+}
+
+.support-card:hover .support-icon {
+    transform: rotate(12deg) scale(1.1);
+}
+
+.support-btn {
+    margin-top: auto;
+    width: 100%;
+    text-align: center;
+    padding: 0.5rem;
+    border-radius: 0.75rem;
+    font-size: 0.75rem;
+    font-weight: bold;
+    color: white;
+    transition: all 0.3s;
+}

BIN
docs/assets/weixin.webp


+ 418 - 0
docs/index.html

@@ -0,0 +1,418 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>TrendRadar 配置文件编辑器</title>
+    <!-- Tailwind CSS -->
+    <script src="https://cdn.tailwindcss.com"></script>
+    <!-- FontAwesome -->
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
+    <!-- js-yaml -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
+    <!-- SortableJS (拖拽排序库) -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
+    <!-- 自定义样式 -->
+    <link rel="stylesheet" href="./assets/style.css">
+</head>
+<body class="bg-gray-100 text-gray-800 font-sans h-screen flex flex-col overflow-hidden">
+
+    <!-- 顶部导航 -->
+    <nav class="bg-white shadow-sm border-b border-gray-200 flex-shrink-0 z-20">
+        <div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between">
+            <a href="https://github.com/sansan0/TrendRadar" target="_blank" class="flex items-center gap-3 hover:opacity-80 transition-opacity">
+                <i class="fa-solid fa-sliders text-blue-600 text-lg"></i>
+                <span class="font-bold text-lg tracking-tight text-gray-900">TrendRadar <span class="text-gray-500 text-xs font-normal ml-2">可视化配置编辑器 </span></span>
+            </a>
+
+            <!-- 隐私安全提示 -->
+            <div class="hidden lg:flex items-center text-xs text-gray-500 bg-gray-50 px-3 py-1.5 rounded-full border border-gray-100 select-none">
+                <i class="fa-solid fa-shield-halved mr-1.5 text-green-500"></i>
+                <span>纯静态页面,数据仅保存在你的本地浏览器,请放心使用</span>
+            </div>
+
+            <div class="flex gap-3">
+                <button onclick="openLoadConfigModal()" class="text-xs text-blue-600 hover:text-blue-800 underline flex items-center gap-1">
+                    <i class="fa-solid fa-cloud-arrow-down"></i>加载官网最新配置
+                </button>
+                <button onclick="copyResult()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded text-sm font-medium transition-colors shadow-sm">
+                    <i class="fa-regular fa-copy mr-1.5"></i>复制配置
+                </button>
+                <button onclick="openSupportModal()" class="bg-gradient-to-r from-orange-400 to-pink-500 hover:from-orange-500 hover:to-pink-600 text-white px-4 py-1.5 rounded text-sm font-medium transition-all shadow-md hover:shadow-lg flex items-center gap-1.5">
+                    <i class="fa-solid fa-heart-pulse"></i>支持项目
+                </button>
+            </div>
+        </div>
+    </nav>
+
+    <!-- 主界面:左右分栏 -->
+    <main class="flex-grow flex overflow-hidden">
+
+        <!-- 左侧:源代码编辑器 (Source) -->
+        <div class="w-1/2 flex flex-col border-r border-gray-200 bg-[#1e1e1e]">
+            <!-- Tab 切换 -->
+            <div class="flex items-center bg-[#252526] border-b border-[#333]">
+                <button id="tab-config" onclick="switchTab('config')" class="tab-button active px-4 py-2 text-xs font-bold text-gray-300 hover:bg-[#2d2d30] transition-colors border-b-2 border-blue-500">
+                    <i class="fa-solid fa-code mr-2"></i>config.yaml
+                </button>
+                <button id="tab-frequency" onclick="switchTab('frequency')" class="tab-button px-4 py-2 text-xs font-bold text-gray-500 hover:bg-[#2d2d30] transition-colors border-b-2 border-transparent">
+                    <i class="fa-solid fa-filter mr-2"></i>frequency_words.txt
+                </button>
+                <div class="flex-grow"></div>
+                <!-- 保存时间显示 -->
+                <div id="save-time-config" class="save-time-badge px-3 text-[10px] text-gray-500 flex items-center gap-1">
+                    <i class="fa-regular fa-clock"></i>
+                    <span id="config-save-label" class="hidden">已保存: </span>
+                    <span id="config-save-time" class="text-gray-400" title="未保存">未保存</span>
+                </div>
+                <div id="save-time-frequency" class="save-time-badge hidden px-3 text-[10px] text-gray-500 flex items-center gap-1">
+                    <i class="fa-regular fa-clock"></i>
+                    <span id="frequency-save-label" class="hidden">已保存: </span>
+                    <span id="frequency-save-time" class="text-gray-400" title="未保存">未保存</span>
+                </div>
+            </div>
+
+            <!-- Config 编辑器 -->
+            <div id="yaml-editor-wrap" class="tab-content highlight-editor-wrap flex-grow w-full h-full bg-[#1e1e1e]">
+                <div id="yaml-backdrop" class="highlight-backdrop"></div>
+                <textarea id="yaml-editor" class="highlight-textarea" spellcheck="false"></textarea>
+            </div>
+
+            <!-- Frequency 编辑器 -->
+            <div id="frequency-editor-wrap" class="tab-content hidden highlight-editor-wrap flex-grow w-full h-full bg-[#1e1e1e]">
+                <div id="frequency-backdrop" class="highlight-backdrop"></div>
+                <textarea id="frequency-editor" class="highlight-textarea" spellcheck="false"></textarea>
+            </div>
+        </div>
+
+        <!-- 右侧:可视化配置 (Visual) -->
+        <div class="w-1/2 flex flex-col bg-gray-50">
+            <div class="flex items-center justify-between px-6 py-3 bg-white border-b border-gray-200">
+                <div class="flex items-center gap-3">
+                    <span class="text-sm font-bold text-gray-700"><i class="fa-solid fa-list-check mr-2"></i><span id="right-panel-title">配置模块</span></span>
+                    <button id="version-check-btn" onclick="checkVersion()" class="text-xs bg-indigo-500 hover:bg-indigo-600 text-white px-3 py-1 rounded shadow-sm transition-all flex items-center gap-1.5" title="检测 config.yaml 版本">
+                        <i class="fa-solid fa-code-compare"></i>
+                        <span>版本检测</span>
+                    </button>
+                    <button onclick="resetToDefault()" class="text-xs text-gray-400 hover:text-red-500 transition-colors px-2 py-1" title="重置当前内容为默认状态">
+                        <i class="fa-solid fa-rotate-left"></i>
+                    </button>
+                </div>
+            </div>
+
+            <!-- 模块导航栏 -->
+            <div id="module-nav" class="tab-content bg-white border-b border-gray-200 px-4 py-2 flex flex-wrap gap-1">
+            </div>
+
+            <!-- Config 可视化面板 -->
+            <div id="config-panel" class="tab-content flex-grow overflow-y-auto p-6 space-y-6">
+            </div>
+
+            <!-- Frequency 可视化面板 -->
+            <div id="frequency-panel" class="tab-content hidden flex-grow overflow-y-auto p-6 space-y-6">
+            </div>
+        </div>
+    </main>
+
+    <!-- RSS 添加弹窗 -->
+    <div id="rss-modal" class="modal-overlay hidden">
+        <div class="modal-content">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-bold text-gray-800"><i class="fa-solid fa-rss mr-2 text-orange-500"></i>添加 RSS 源</h3>
+                <button onclick="closeRssModal()" class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-times text-xl"></i></button>
+            </div>
+            <div class="space-y-4">
+                <div>
+                    <label class="block text-xs font-bold text-gray-600 mb-1">源 ID(唯一标识,英文)</label>
+                    <input type="text" id="rss-id" placeholder="例如: my-blog" class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+                </div>
+                <div>
+                    <label class="block text-xs font-bold text-gray-600 mb-1">显示名称</label>
+                    <input type="text" id="rss-name" placeholder="例如: 我的博客" class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+                </div>
+                <div>
+                    <label class="block text-xs font-bold text-gray-600 mb-1">RSS URL</label>
+                    <input type="text" id="rss-url" placeholder="https://example.com/feed.xml" class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+                </div>
+                <div>
+                    <label class="block text-xs font-bold text-gray-600 mb-1">最大文章年龄(天,可选)</label>
+                    <input type="number" id="rss-max-age" placeholder="留空使用全局设置" class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+                </div>
+            </div>
+
+            <!-- RSS 灵感折叠区 -->
+            <div class="mt-5 border-t border-gray-100 pt-4">
+                <button type="button" onclick="toggleRssTips()" class="w-full flex items-center justify-between text-xs text-orange-600 hover:text-orange-700 bg-orange-50 hover:bg-orange-100 px-3 py-2 rounded-lg transition-all group">
+                    <span class="font-bold flex items-center gap-1.5">
+                        <i class="fa-regular fa-lightbulb"></i> RSS 订阅灵感 & 参考库 <span class="font-normal opacity-70 ml-1">(内附常用源)</span>
+                    </span>
+                    <i id="rss-tips-icon" class="fa-solid fa-chevron-down transition-transform duration-200 text-orange-400 group-hover:text-orange-600" style="transform: rotate(180deg);"></i>
+                </button>
+
+                <div id="rss-tips-panel" class="mt-2 space-y-3 pl-1">
+
+                    <!-- 必应新闻 -->
+                    <div class="bg-white border border-gray-100 rounded-lg p-3 shadow-sm">
+                        <div class="flex items-center gap-2 mb-2">
+                            <i class="fa-brands fa-microsoft text-blue-500"></i>
+                            <span class="font-bold text-gray-700">Bing 新闻 (支持任意关键词)</span>
+                        </div>
+                        <div class="grid grid-cols-2 gap-2 mb-2">
+                             <button onclick="fillRssUrl('https://www.bing.com/news/search?q=科技+编程&format=RSS')" class="text-left text-[10px] border border-gray-200 hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 rounded px-2 py-1.5 transition-colors truncate" title="点击填入">
+                                🚀 科技/编程
+                            </button>
+                             <button onclick="fillRssUrl('https://www.bing.com/news/search?q=全球新闻&format=RSS')" class="text-left text-[10px] border border-gray-200 hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 rounded px-2 py-1.5 transition-colors truncate" title="点击填入">
+                                🌍 全球新闻
+                            </button>
+                             <button onclick="fillRssUrl('https://www.bing.com/news/search?q=人工智能&format=RSS')" class="text-left text-[10px] border border-gray-200 hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 rounded px-2 py-1.5 transition-colors truncate" title="点击填入">
+                                🤖 人工智能
+                            </button>
+                             <button onclick="fillRssUrl('https://www.bing.com/news/search?q=黄金价格+走势&format=RSS')" class="text-left text-[10px] border border-gray-200 hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 rounded px-2 py-1.5 transition-colors truncate" title="点击填入">
+                                💰 黄金/财经
+                            </button>
+                        </div>
+                        <div class="text-[10px] text-gray-400">
+                            💡 小贴士:修改 URL 中的 <code class="bg-gray-100 px-1 rounded text-gray-600">q=</code> 参数即可监控任何你感兴趣的话题。
+                        </div>
+                    </div>
+
+
+                    <!-- 更多参考 -->
+                    <div class="bg-white border border-gray-100 rounded-lg p-3 shadow-sm">
+                        <div class="flex items-center gap-2 mb-2">
+                            <i class="fa-solid fa-book-open text-purple-500"></i>
+                            <span class="font-bold text-gray-700">更多 RSS 源参考</span>
+                        </div>
+                        <div class="flex flex-wrap gap-2 text-xs">
+                             <a href="https://github.com/tuan3w/awesome-tech-rss" target="_blank" class="text-blue-600 hover:underline flex items-center bg-blue-50 px-2 py-1 rounded">
+                                <i class="fa-brands fa-github mr-1"></i>科技/编程
+                            </a>
+                             <a href="https://github.com/plenaryapp/awesome-rss-feeds" target="_blank" class="text-blue-600 hover:underline flex items-center bg-blue-50 px-2 py-1 rounded">
+                                <i class="fa-brands fa-github mr-1"></i>全球新闻
+                            </a>
+                        </div>
+                    </div>
+
+                    <!-- 免责声明 -->
+                    <div class="text-[10px] text-gray-400 italic leading-relaxed px-1">
+                        <i class="fa-solid fa-shield-halved mr-1 text-gray-300"></i>免责声明:以上 RSS 示例及第三方工具均源自互联网,开发者未一一验证其长期有效性,请你在使用前自行核实。
+                    </div>
+                </div>
+            </div>
+
+            <div class="flex justify-end gap-2 mt-6">
+                <button onclick="closeRssModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">取消</button>
+                <button onclick="confirmAddRss()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">添加</button>
+            </div>
+        </div>
+    </div>
+
+    <!-- 平台添加弹窗 -->
+    <div id="platform-modal" class="modal-overlay hidden">
+        <div class="modal-content">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-bold text-gray-800"><i class="fa-solid fa-layer-group mr-2 text-green-600"></i>添加热榜平台</h3>
+                <button onclick="closePlatformModal()" class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-times text-xl"></i></button>
+            </div>
+
+            <!-- 标签页切换 -->
+            <div class="flex border-b border-gray-200 mb-4">
+                <button onclick="switchPlatformTab('select')" id="tab-platform-select" class="flex-1 py-2 text-sm font-bold text-blue-600 border-b-2 border-blue-600 transition-colors">
+                    <i class="fa-solid fa-list mr-1"></i>选择预设
+                </button>
+                <button onclick="switchPlatformTab('custom')" id="tab-platform-custom" class="flex-1 py-2 text-sm font-bold text-gray-500 border-b-2 border-transparent hover:text-gray-700 transition-colors">
+                    <i class="fa-solid fa-pen-to-square mr-1"></i>手动输入
+                </button>
+            </div>
+
+            <!-- 1. 选择预设平台 -->
+            <div id="platform-select-panel" class="space-y-4">
+                <div id="available-platforms-list" class="space-y-2 max-h-60 overflow-y-auto pr-1">
+                    <!-- 动态生成可用平台 -->
+                </div>
+                <div id="no-platforms-tip" class="hidden text-center py-6 text-gray-500 text-sm bg-gray-50 rounded">
+                    <i class="fa-solid fa-check-circle text-green-500 mr-2"></i>所有预设平台已添加
+                </div>
+            </div>
+
+            <!-- 2. 手动输入平台 -->
+            <div id="platform-custom-panel" class="hidden space-y-4">
+                <div class="bg-blue-50 border border-blue-100 rounded p-3 mb-3 text-xs text-blue-800">
+                    <i class="fa-solid fa-info-circle mr-1"></i>自定义平台需要后端爬虫支持,此处仅用于配置占位。
+                </div>
+                <div>
+                    <label class="block text-xs font-bold text-gray-600 mb-1">平台 Key(英文)</label>
+                    <input type="text" id="custom-platform-key" placeholder="例如: sspai" class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+                </div>
+                <div>
+                    <label class="block text-xs font-bold text-gray-600 mb-1">显示名称</label>
+                    <input type="text" id="custom-platform-name" placeholder="例如: 少数派" class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+                </div>
+            </div>
+
+            <div class="flex justify-end gap-2 mt-6">
+                <button onclick="closePlatformModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">取消</button>
+                <button onclick="confirmAddPlatform()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">添加</button>
+            </div>
+        </div>
+    </div>
+
+    <!-- 词组类型选择弹窗 -->
+    <div id="wordgroup-type-modal" class="modal-overlay hidden">
+        <div class="modal-content max-w-2xl">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-bold text-gray-800"><i class="fa-solid fa-layer-group mr-2 text-blue-500"></i>选择词组类型</h3>
+                <button onclick="closeWordGroupTypeModal()" class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-times text-xl"></i></button>
+            </div>
+            <div class="space-y-3">
+                <!-- 组别名类型 -->
+                <div onclick="confirmAddWordGroup('group')" class="cursor-pointer border-2 border-orange-200 bg-orange-50 rounded-lg p-4 hover:border-orange-400 hover:bg-orange-100 transition-all">
+                    <div class="flex items-center gap-3">
+                        <span class="text-xs bg-orange-500 text-white px-2 py-1 rounded font-bold">组别名</span>
+                        <span class="font-bold text-gray-800">多关键词词组(推荐)</span>
+                    </div>
+                    <div class="mt-2 text-sm text-gray-600">
+                        <div class="font-mono bg-white rounded p-2 text-xs border border-orange-200">
+                            <div class="text-orange-600">[东亚]</div>
+                            <div>日本</div>
+                            <div>韩国</div>
+                            <div>朝鲜</div>
+                        </div>
+                        <div class="mt-2 text-xs text-gray-500">
+                            <i class="fa-solid fa-check-circle text-orange-500 mr-1"></i>适用于:多个关键词归为一组,统一显示为组名
+                        </div>
+                    </div>
+                </div>
+                <!-- 单个别名类型 -->
+                <div onclick="confirmAddWordGroup('alias')" class="cursor-pointer border-2 border-teal-200 bg-teal-50 rounded-lg p-4 hover:border-teal-400 hover:bg-teal-100 transition-all">
+                    <div class="flex items-center gap-3">
+                        <span class="text-xs bg-teal-500 text-white px-2 py-1 rounded font-bold">单个别名</span>
+                        <span class="font-bold text-gray-800">正则/关键词 + 别名</span>
+                    </div>
+                    <div class="mt-2 text-sm text-gray-600">
+                        <div class="font-mono bg-white rounded p-2 text-xs border border-teal-200">
+                            <div>/胖东来|于东来/ <span class="text-teal-600">=></span> 胖东来</div>
+                        </div>
+                        <div class="mt-2 text-xs text-gray-500">
+                            <i class="fa-solid fa-check-circle text-teal-500 mr-1"></i>适用于:用正则匹配多个词,显示为一个别名(前后有空行分隔)
+                        </div>
+                    </div>
+                </div>
+                <!-- 连续别名类型 -->
+                <div onclick="confirmAddWordGroup('multi-alias')" class="cursor-pointer border-2 border-purple-200 bg-purple-50 rounded-lg p-4 hover:border-purple-400 hover:bg-purple-100 transition-all">
+                    <div class="flex items-center gap-3">
+                        <span class="text-xs bg-purple-500 text-white px-2 py-1 rounded font-bold">连续别名组</span>
+                        <span class="font-bold text-gray-800">多个相关品牌/词组</span>
+                    </div>
+                    <div class="mt-2 text-sm text-gray-600">
+                        <div class="font-mono bg-white rounded p-2 text-xs border border-purple-200">
+                            <div>/智元|灵犀|稚晖君/ <span class="text-purple-600">=></span> 智元机器人</div>
+                            <div>/众擎|EngineAI/ <span class="text-purple-600">=></span> 众擎机器人</div>
+                        </div>
+                        <div class="mt-2 text-xs text-gray-500">
+                            <i class="fa-solid fa-check-circle text-purple-500 mr-1"></i>适用于:多个相关品牌放在一起(<strong>无空行分隔</strong>)
+                        </div>
+                    </div>
+                </div>
+                <!-- 普通词组类型 -->
+                <div onclick="confirmAddWordGroup('plain')" class="cursor-pointer border-2 border-gray-200 bg-gray-50 rounded-lg p-4 hover:border-gray-400 hover:bg-gray-100 transition-all">
+                    <div class="flex items-center gap-3">
+                        <span class="text-xs bg-gray-500 text-white px-2 py-1 rounded font-bold">普通词组</span>
+                        <span class="font-bold text-gray-800">简单关键词</span>
+                    </div>
+                    <div class="mt-2 text-sm text-gray-600">
+                        <div class="font-mono bg-white rounded p-2 text-xs border border-gray-200">
+                            <div>申奥</div>
+                        </div>
+                        <div class="mt-2 text-xs text-gray-500">
+                            <i class="fa-solid fa-check-circle text-gray-500 mr-1"></i>适用于:单个或少量普通关键词
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="flex justify-end gap-2 mt-6">
+                <button onclick="closeWordGroupTypeModal()" class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">取消</button>
+            </div>
+        </div>
+    </div>
+
+    <!-- 支持项目弹窗 -->
+    <div id="support-modal" class="modal-overlay hidden" onclick="closeSupportModalOutside(event)">
+        <div class="modal-content support-modal-content max-w-5xl w-[95%] max-h-[90vh] overflow-y-auto p-8">
+            <div class="flex items-center justify-between mb-8">
+                <div class="flex items-center gap-4">
+                    <div class="w-12 h-12 bg-orange-50 rounded-full flex items-center justify-center text-orange-500 shadow-sm relative overflow-hidden">
+                        <div class="absolute inset-0 bg-orange-400 opacity-20 animate-ping"></div>
+                        <i class="fa-solid fa-heart text-2xl animate-pulse relative z-10"></i>
+                    </div>
+                    <div>
+                        <h3 class="text-2xl font-bold text-gray-800 tracking-tight">觉得好用?支持一下 ✨</h3>
+                        <p class="text-sm text-gray-500 mt-1">TrendRadar 完全开源免费,你的每一次支持都是作者更新的动力</p>
+                    </div>
+                </div>
+                <button onclick="closeSupportModal()" class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-100 text-gray-400 transition-colors">
+                    <i class="fa-solid fa-times text-xl"></i>
+                </button>
+            </div>
+
+            <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
+                <a href="https://github.com/sansan0/TrendRadar" target="_blank" class="support-card group border-orange-200 bg-orange-50/30">
+                    <div class="absolute top-0 right-0 bg-gradient-to-r from-orange-400 to-red-500 text-white text-[10px] px-2 py-0.5 rounded-bl-lg font-bold shadow-sm z-10">推荐支持</div>
+                    <div class="support-card-num opacity-50">01</div>
+                    <div class="support-icon text-orange-500 bg-orange-100 group-hover:bg-orange-200 mb-4 group-hover:scale-110 transition-transform">
+                        <i class="fa-solid fa-star text-2xl"></i>
+                    </div>
+                    <h4 class="text-lg font-bold text-gray-800 mb-2">点亮 Star</h4>
+                    <p class="text-sm text-gray-500 mb-6 text-center leading-relaxed">免费且重要!<br>只需 1 秒,让更多人发现它</p>
+                    <span class="support-btn bg-gradient-to-r from-orange-400 to-red-500 shadow-lg shadow-orange-200 group-hover:shadow-xl group-hover:from-orange-500 group-hover:to-red-600">立即前往 GitHub</span>
+                </a>
+
+                <div class="support-card group">
+                    <div class="absolute top-0 right-0 bg-gradient-to-r from-green-400 to-emerald-500 text-white text-[10px] px-2 py-0.5 rounded-bl-lg font-bold shadow-sm z-10">订阅更新</div>
+                    <div class="support-card-num">02</div>
+                    <div class="support-icon text-green-600 bg-green-50 group-hover:bg-green-100 mb-4">
+                        <i class="fa-brands fa-weixin text-2xl"></i>
+                    </div>
+                    <h4 class="text-lg font-bold text-gray-800 mb-2">不迷路</h4>
+                    <p class="text-sm text-gray-500 mb-4 text-center">关注公众号<br>第一时间获取更新通知</p>
+                    <div class="w-36 h-36 bg-white border border-gray-100 rounded-xl p-2 shadow-sm group-hover:shadow-md transition-shadow">
+                        <img src="./assets/weixin.webp" alt="微信公众号" class="w-full h-full object-contain">
+                    </div>
+                    <p class="text-xs text-gray-400 mt-3">扫码加入社区</p>
+                </div>
+
+                <div class="support-card group">
+                    <div class="absolute top-0 right-0 bg-gradient-to-r from-emerald-400 to-teal-500 text-white text-[10px] px-2 py-0.5 rounded-bl-lg font-bold shadow-sm z-10">随心鼓励</div>
+                    <div class="support-card-num">03</div>
+                    <div class="support-icon text-emerald-600 bg-emerald-50 group-hover:bg-emerald-100 mb-4">
+                        <i class="fa-solid fa-hand-holding-heart text-2xl"></i>
+                    </div>
+                    <h4 class="text-lg font-bold text-gray-800 mb-2">随心赞赏</h4>
+                    <p class="text-sm text-gray-500 mb-4 text-center">一瓶水、一包辣条都是爱<br>金额随意,1 元也是动力</p>
+                    <div class="w-36 h-36 bg-white border border-gray-100 rounded-xl p-2 shadow-sm group-hover:shadow-md transition-shadow">
+                        <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2026%2F01%2F18ecce7c224ce0ea4c59394c29e408f8-e0d1db45.webp" alt="微信支付" class="w-full h-full object-contain">
+                    </div>
+                    <p class="text-xs text-gray-400 mt-3">微信扫码 • 丰俭由人</p>
+                </div>
+
+                <a href="https://sansan0.github.io/mao-map/" target="_blank" class="support-card group">
+                    <div class="absolute top-0 right-0 bg-gradient-to-r from-red-400 to-rose-500 text-white text-[10px] px-2 py-0.5 rounded-bl-lg font-bold shadow-sm z-10">探索发现</div>
+                    <div class="support-card-num">04</div>
+                    <div class="support-icon text-red-500 bg-red-50 group-hover:bg-red-100 mb-4">
+                        <i class="fa-solid fa-map-location-dot text-2xl"></i>
+                    </div>
+                    <h4 class="text-lg font-bold text-gray-800 mb-2">探索更多</h4>
+                    <p class="text-sm text-gray-500 mb-6 text-center leading-relaxed">历史足迹地图<br>另一个用心的作品</p>
+                    <span class="support-btn bg-red-50 text-red-600 group-hover:bg-red-100 group-hover:text-red-700 border border-red-100">去看看</span>
+                </a>
+            </div>
+
+            <div class="mt-8 pt-6 border-t border-gray-100 text-center">
+                <p class="text-sm text-gray-400 font-serif italic tracking-wide">“江山如此多娇,引无数英雄竞折腰”</p>
+            </div>
+        </div>
+    </div>
+
+    <script src="./assets/script.js"></script>
+</body>
+</html>

+ 1 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [project]
 name = "trendradar"
-version = "5.4.0"
+version = "5.5.0"
 description = "TrendRadar - 热点新闻聚合与分析工具"
 requires-python = ">=3.10"
 dependencies = [

+ 1 - 1
trendradar/__init__.py

@@ -9,5 +9,5 @@ TrendRadar - 热点新闻聚合与分析工具
 
 from trendradar.context import AppContext
 
-__version__ = "5.4.0"
+__version__ = "5.5.0"
 __all__ = ["AppContext", "__version__"]

+ 1 - 1
version

@@ -1 +1 @@
-5.4.0
+5.5.0

+ 2 - 2
version_configs

@@ -1,4 +1,4 @@
-config.yaml=1.0.0
-frequency_words.txt=1.0.0
+config.yaml=1.1.0
+frequency_words.txt=1.1.0
 ai_analysis_prompt.txt=1.0.0
 ai_translation_prompt.txt=1.0.0