因为Chrome 频繁的对 TT Rss的标记,甚烦!加之国庆无聊,准备在国庆假期用GPT 来手搓一个极简的自用工具。肝了一晚上,终于提前上线 简阅RSS
image-1727562979115

总体上就是,参照TT Rss 做的,因为GPT对CSS的回答和预期差太多,所以花费时间也比预期的长,只能暂时这样,等后面有时间,再搞它。现在只能在PC的Web端可用,对我来已经够用。

前端

因为初衷就是本着,极简自用,所以就是单个页面,虽然GPT在CSS方面回答上,不容易对焦。但只是需要多试几次而已,我只是把TT Rss 的界面截图发给它,然后它就给我生成这个页面。然后在这个页面慢慢问,不停的问就行了。
image-1727563451916

<template>
  <div class="app-layout">
    <div class="main-content">
      <!-- Sidebar (left) -->
      <div class="resizable-sidebar" :style="{ width: sidebarWidth + 'px' }">
        <h6 class="p-2">订阅</h6>
        <ul class="list-group">
          <li v-for="group in treeGroups" :key="group.id" class="list-group-item">
            <strong @click="toggleGroup(group)" class="group-header">
              <span>{{ group.isOpen ? '[-]' : '[+]' }}</span> 
              {{ group.name }}
            </strong>

            <ul v-show="group.isOpen">
              <li v-for="node in getNodesForGroup(group.id)" :key="node.id" class="tree-node" 
              :class="{ active: selectedNode === node }"
              @click="selectFeedNode(node)" >
                <img :src="'data:image/png;base64,' + node.siteIcon" alt="icon" />
                {{ node.siteName }} 
                <span class="node-count">{{ node.totalFeed }}</span>
              </li>
            </ul>
          </li>
        </ul>
      </div>

      <!-- Resizable divider (vertical) -->
      <div class="divider-vertical" @mousedown="startResizingVertical"></div>

      <div class="content-area" ref="contentArea">
        <!-- Article List (顶部部分) -->
        <div class="article-list p-3" :style="{ height: articleListHeight + 'px' }">
          <h6>{{selectedNode?selectedNode.siteName :''}}-文章列表</h6>
          <ul class="list-group">
            <li
              v-for="article in selectedArticles"
              :key="article.id"
              class="list-article-item"
              @click="selectArticle(article)"
            > 
              <input type="checkbox" class="checkbox" :checked="article.isSelected" @change="toggleSelectArticle(article)" />
              <img src="@/assets/images/star.svg" class="icon bookmark-icon" alt="bookmark icon" /> 
              <img src="@/assets/images/rss.svg" class="icon rss-icon" alt="rss icon" />  
              <span class="article-title">{{ article.title }}</span> 
              <span class="pub-date">{{ article.pubDate }}</span>  
            </li>
          </ul>
        </div>

        <!-- Divider for resizing -->
        <div class="divider-horizontal" @mousedown="startResizingHorizontal"></div>

        <!-- Article Summary (底部部分) -->
        <div class="article-summary p-3" :style="{ height: articleSummaryHeight + 'px', overflowY: 'auto' }">
          <a v-if="itm" :href="itm.link || '#'" target="_blank">{{ itm?.title || "" }}</a>
          <div v-html="itm?.excerpt || ' '"></div>
        </div>
      </div>
    </div>
  </div>
</template>

    <script src="./AppLayout.js"></script>
    <style src="@/assets/styles/AppLayout.css" scoped></style>

后端

后端虽然代码多,Feed多就得啰嗦,有的给Feed ,直接用 SyndicationFeed 就能解析,有的web直接一个request 就能拿到html,也是扔给GPT,就能出代码。稍麻烦的需要认证的就得跑 playwright把cookie装上,下次请求时自动更新它。 但GPT给力,基本上一问就是标准答案,所以花的时间反而最少的。
image-1727563784201

PW.cs

using Microsoft.Playwright;

namespace RssAdapter.Common
{
    public class PW
    {
        private static IPlaywright _playwright;
        private static IBrowser _browser;
        private static IBrowserContext _context;

        private static readonly object _lock = new object();

        public static async Task<IBrowserContext> GetBrowserContextAsync()
        {
            if (_context == null)
            {
                lock (_lock)
                {
                    if (_context == null)
                    {
                        _playwright = Playwright.CreateAsync().Result;
                        _browser = _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions  { Headless = true }).Result;
                        _context = _browser.NewContextAsync( new BrowserNewContextOptions
                        {
                            UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
                            ViewportSize = new ViewportSize { Width = 1920, Height = 1080 }, 
                            //Locale = "en-US", // 模拟你本地的语言环境
                            //TimezoneId = "America/New_York", // 模拟时区
                            BypassCSP = true,
                            JavaScriptEnabled = true
                        }).Result;
                    }
                }
            }
            return _context;
        }
    }
}

linuxdo.cs

public async Task<IActionResult> Getlatest()
{
    try
    {
        string rssFeedUrl = "https://linux.do/latest.rss";

        var uri = new Uri(rssFeedUrl);
        string CookieFilePath = Path.Combine(AppContext.BaseDirectory, "cookies", uri.Host);
        var context = await PW.GetBrowserContextAsync();
        if (System.IO.File.Exists(CookieFilePath))
        {
            // 从本地加载 cookies 并添加到 context
            var cookies = LoadCookiesFromFile(CookieFilePath);
            if (cookies != null)
            {
                await context.AddCookiesAsync(cookies);
                Console.WriteLine("Cookies loaded from file.");
            }
        }
        var page = await context.NewPageAsync();
        await page.GotoAsync(rssFeedUrl, new PageGotoOptions { Timeout = 60000 });
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
        string content = await page.ContentAsync();
        var updatedCookies = await context.CookiesAsync();
        SaveCookiesToFile(updatedCookies, CookieFilePath);
        string xmlContent = ExtractXmlFromHtml(content);
        string schemeAndHost = $"{uri.Scheme}://{uri.Host}";
        using (XmlReader reader = XmlReader.Create(new System.IO.StringReader(xmlContent)))
        {
            SyndicationFeed feed = SyndicationFeed.Load(reader); 
            feed.Links.First().Uri = new Uri(rssFeedUrl);
            RSS2RSS rSS = new RSS2RSS(_configuration);
            rSS.SaveFeedToDatabase(feed);
            var responseData = rSS.ConvertFeedToString(feed);
            await page.CloseAsync();
            return Ok(responseData);
        }
    }
    catch (Exception ex)
    {
        return BadRequest($"Failed to retrieve content. Status code: {ex.Message}");
    }
}

后续

这是一个开始,RSS 聚合本身就是Feed内容一个载体,需要不断更新和积累,它就是为信息和数据的连接而产生的,“数据”是客观量化的结果,而”信息“ 有传递变化之意。

感谢TT Rss ,它给使用Rss 提供了方便。
感谢 ssh-news.top 的创意,它极大触动了我,让我有自建Web to Rss的动力,把它运行在IDE的终端里,没人分清楚你在上网还是调试代码。
最后,天佑合肥!