feat: add RSS account type - posts link directly without AI, update admin UI
This commit is contained in:
parent
da5a366cc1
commit
d8988dc870
|
|
@ -4,7 +4,7 @@ import AdminShell from '@/components/AdminShell';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Plus, Play, Eye, Trash2, Power, PowerOff, RefreshCw, Newspaper,
|
Plus, Play, Eye, Trash2, Power, PowerOff, RefreshCw, Newspaper, Rss,
|
||||||
ChevronDown, ChevronUp, Bot, Clock, AlertCircle, CheckCircle, ExternalLink,
|
ChevronDown, ChevronUp, Bot, Clock, AlertCircle, CheckCircle, ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -270,7 +270,7 @@ export default function OfficialAccountsPage() {
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-4 flex items-center gap-4">
|
<div className="p-4 flex items-center gap-4">
|
||||||
<div className="w-10 h-10 bg-brand-100 rounded-full flex items-center justify-center flex-shrink-0">
|
<div className="w-10 h-10 bg-brand-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
{cfg.account_type === 'news' ? <Newspaper className="w-5 h-5 text-brand-600" /> : <Bot className="w-5 h-5 text-brand-600" />}
|
{cfg.account_type === 'news' ? <Newspaper className="w-5 h-5 text-brand-600" /> : cfg.account_type === 'rss' ? <Rss className="w-5 h-5 text-brand-600" /> : <Bot className="w-5 h-5 text-brand-600" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -344,7 +344,7 @@ export default function OfficialAccountsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cfg.account_type === 'news' && sources.length > 0 && (
|
{(cfg.account_type === 'news' || cfg.account_type === 'rss') && sources.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-medium text-gray-600">News Sources:</span>
|
<span className="text-sm font-medium text-gray-600">News Sources:</span>
|
||||||
<div className="mt-1 space-y-1">
|
<div className="mt-1 space-y-1">
|
||||||
|
|
@ -396,6 +396,9 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
|
||||||
if (accountType === 'news') {
|
if (accountType === 'news') {
|
||||||
setNewsSources(DEFAULT_NEWS_SOURCES);
|
setNewsSources(DEFAULT_NEWS_SOURCES);
|
||||||
setSystemPrompt(DEFAULT_NEWS_PROMPT);
|
setSystemPrompt(DEFAULT_NEWS_PROMPT);
|
||||||
|
} else if (accountType === 'rss') {
|
||||||
|
setNewsSources(DEFAULT_NEWS_SOURCES);
|
||||||
|
setSystemPrompt('');
|
||||||
} else {
|
} else {
|
||||||
setNewsSources([]);
|
setNewsSources([]);
|
||||||
setSystemPrompt(DEFAULT_GENERAL_PROMPT);
|
setSystemPrompt(DEFAULT_GENERAL_PROMPT);
|
||||||
|
|
@ -441,17 +444,22 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
|
||||||
<select value={accountType} onChange={(e) => setAccountType(e.target.value)}
|
<select value={accountType} onChange={(e) => setAccountType(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm">
|
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm">
|
||||||
<option value="general">General</option>
|
<option value="general">General</option>
|
||||||
<option value="news">News</option>
|
<option value="news">News (AI Commentary)</option>
|
||||||
|
<option value="rss">RSS (Link Only)</option>
|
||||||
<option value="community">Community</option>
|
<option value="community">Community</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{accountType !== 'rss' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
||||||
<ModelSelector value={modelId} onChange={setModelId} />
|
<ModelSelector value={modelId} onChange={setModelId} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4 mb-4">
|
<div className="grid grid-cols-4 gap-4 mb-4">
|
||||||
|
{accountType !== 'rss' && (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Temperature</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Temperature</label>
|
||||||
<input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))}
|
<input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))}
|
||||||
|
|
@ -462,6 +470,8 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
|
||||||
<input type="number" min="50" max="4000" value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value))}
|
<input type="number" min="50" max="4000" value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value))}
|
||||||
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm" />
|
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Interval (min)</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Interval (min)</label>
|
||||||
<input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))}
|
<input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))}
|
||||||
|
|
@ -474,13 +484,15 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{accountType !== 'rss' && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">System Prompt</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">System Prompt</label>
|
||||||
<textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={6}
|
<textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={6}
|
||||||
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm font-mono" />
|
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm font-mono" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{accountType === 'news' && (
|
{(accountType === 'news' || accountType === 'rss') && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">News Sources (RSS Feeds)</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">News Sources (RSS Feeds)</label>
|
||||||
{newsSources.map((src, i) => (
|
{newsSources.map((src, i) => (
|
||||||
|
|
@ -566,6 +578,8 @@ function EditAccountForm({ config, onDone }: { config: Config; onDone: () => voi
|
||||||
<div className="border-t border-warm-200 pt-3 mt-3">
|
<div className="border-t border-warm-200 pt-3 mt-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 mb-3">Edit Configuration</h3>
|
<h3 className="text-sm font-semibold text-gray-700 mb-3">Edit Configuration</h3>
|
||||||
<div className="grid grid-cols-4 gap-3 mb-3">
|
<div className="grid grid-cols-4 gap-3 mb-3">
|
||||||
|
{config.account_type !== 'rss' && (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">Model</label>
|
<label className="block text-xs font-medium text-gray-600 mb-1">Model</label>
|
||||||
<ModelSelector value={modelId} onChange={setModelId} />
|
<ModelSelector value={modelId} onChange={setModelId} />
|
||||||
|
|
@ -575,6 +589,8 @@ function EditAccountForm({ config, onDone }: { config: Config; onDone: () => voi
|
||||||
<input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))}
|
<input type="number" step="0.1" min="0" max="2" value={temperature} onChange={(e) => setTemperature(Number(e.target.value))}
|
||||||
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" />
|
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">Interval (min)</label>
|
<label className="block text-xs font-medium text-gray-600 mb-1">Interval (min)</label>
|
||||||
<input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))}
|
<input type="number" min="5" value={intervalMin} onChange={(e) => setIntervalMin(Number(e.target.value))}
|
||||||
|
|
@ -586,13 +602,15 @@ function EditAccountForm({ config, onDone }: { config: Config; onDone: () => voi
|
||||||
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" />
|
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{config.account_type !== 'rss' && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">System Prompt</label>
|
<label className="block text-xs font-medium text-gray-600 mb-1">System Prompt</label>
|
||||||
<textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={4}
|
<textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} rows={4}
|
||||||
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs font-mono" />
|
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs font-mono" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{config.account_type === 'news' && (
|
{(config.account_type === 'news' || config.account_type === 'rss') && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">News Sources</label>
|
<label className="block text-xs font-medium text-gray-600 mb-1">News Sources</label>
|
||||||
{newsSources.map((src, i) => (
|
{newsSources.map((src, i) => (
|
||||||
|
|
|
||||||
|
|
@ -668,9 +668,12 @@ func (s *OfficialAccountsService) runScheduledPosts() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time to post!
|
// Time to post!
|
||||||
if c.AccountType == "news" {
|
switch c.AccountType {
|
||||||
|
case "news":
|
||||||
s.scheduleNewsPost(ctx, c.ID)
|
s.scheduleNewsPost(ctx, c.ID)
|
||||||
} else {
|
case "rss":
|
||||||
|
s.scheduleRSSPost(ctx, c.ID)
|
||||||
|
default:
|
||||||
s.scheduleGeneralPost(ctx, c.ID)
|
s.scheduleGeneralPost(ctx, c.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -702,6 +705,32 @@ func (s *OfficialAccountsService) scheduleNewsPost(ctx context.Context, configID
|
||||||
_ = body // logged implicitly via post
|
_ = body // logged implicitly via post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OfficialAccountsService) scheduleRSSPost(ctx context.Context, configID string) {
|
||||||
|
items, sourceNames, err := s.FetchNewArticles(ctx, configID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("config", configID).Msg("[OfficialAccounts] Failed to fetch RSS articles")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
log.Debug().Str("config", configID).Msg("[OfficialAccounts] No new RSS articles to post")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post the first new article — body is just the link
|
||||||
|
article := items[0]
|
||||||
|
sourceName := sourceNames[0]
|
||||||
|
body := article.Link
|
||||||
|
|
||||||
|
postID, err := s.CreatePostForAccount(ctx, configID, body, &article, sourceName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("config", configID).Msg("[OfficialAccounts] Failed to create RSS post")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("config", configID).Str("post_id", postID).Str("source", sourceName).Str("title", article.Title).Str("link", body).Msg("[OfficialAccounts] RSS post created")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OfficialAccountsService) scheduleGeneralPost(ctx context.Context, configID string) {
|
func (s *OfficialAccountsService) scheduleGeneralPost(ctx context.Context, configID string) {
|
||||||
postID, body, err := s.GenerateAndPost(ctx, configID, nil, "")
|
postID, body, err := s.GenerateAndPost(ctx, configID, nil, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue