From 5ca4d1c911c4b4612f78f9d8a03be8a79b9be9e7 Mon Sep 17 00:00:00 2001
From: Jesse <15653378+squarezw@user.noreply.gitee.com>
Date: Wed, 16 Sep 2026 06:41:23 +0800
Subject: [PATCH] feat(apps): simplify presentation modes
---
app/apps/[id]/page.tsx | 6 +-
.../subscription-agent/FeedItemsDialog.tsx | 259 ------
.../ScheduleConfigSection.tsx | 130 ---
.../SummaryDetailDialog.tsx | 272 ------
.../subscription-agent/SummaryListDialog.tsx | 352 --------
app/apps/page.tsx | 503 ++---------
docs/subscription-agent.md | 837 ------------------
env.example | 4 -
lib/appTrigger.ts | 26 +-
lib/cron/index.ts | 13 +-
lib/cron/subscription-scheduler.ts | 144 ---
lib/subscription-agent/generate-summary.ts | 58 --
messages/en/apps.json | 26 +-
messages/zh-CN/apps.json | 30 +-
pages/api/v1/apps/[id].ts | 10 -
pages/api/v1/subscription-agent/feeds/[id].ts | 63 --
.../v1/subscription-agent/feeds/[id]/items.ts | 55 --
.../api/v1/subscription-agent/feeds/index.ts | 65 --
pages/api/v1/subscription-agent/fetch.ts | 44 -
.../v1/subscription-agent/summaries/[id].ts | 47 -
.../summaries/generate/today.ts | 22 -
.../summaries/generate/week.ts | 22 -
.../v1/subscription-agent/summaries/index.ts | 62 --
test/appPresentationModeCleanup.test.ts | 22 +
test/appTrigger.test.ts | 35 +-
types/subscription-agent.ts | 110 ---
types/workflow.ts | 3 +-
27 files changed, 110 insertions(+), 3110 deletions(-)
delete mode 100644 app/apps/components/subscription-agent/FeedItemsDialog.tsx
delete mode 100644 app/apps/components/subscription-agent/ScheduleConfigSection.tsx
delete mode 100644 app/apps/components/subscription-agent/SummaryDetailDialog.tsx
delete mode 100644 app/apps/components/subscription-agent/SummaryListDialog.tsx
delete mode 100644 docs/subscription-agent.md
delete mode 100644 lib/cron/subscription-scheduler.ts
delete mode 100644 lib/subscription-agent/generate-summary.ts
delete mode 100644 pages/api/v1/subscription-agent/feeds/[id].ts
delete mode 100644 pages/api/v1/subscription-agent/feeds/[id]/items.ts
delete mode 100644 pages/api/v1/subscription-agent/feeds/index.ts
delete mode 100644 pages/api/v1/subscription-agent/fetch.ts
delete mode 100644 pages/api/v1/subscription-agent/summaries/[id].ts
delete mode 100644 pages/api/v1/subscription-agent/summaries/generate/today.ts
delete mode 100644 pages/api/v1/subscription-agent/summaries/generate/week.ts
delete mode 100644 pages/api/v1/subscription-agent/summaries/index.ts
create mode 100644 test/appPresentationModeCleanup.test.ts
delete mode 100644 types/subscription-agent.ts
diff --git a/app/apps/[id]/page.tsx b/app/apps/[id]/page.tsx
index da4e3ef..e911420 100644
--- a/app/apps/[id]/page.tsx
+++ b/app/apps/[id]/page.tsx
@@ -2,7 +2,7 @@
import { use, useState, useEffect } from "react";
import { useTranslations } from "next-intl";
-import { triggerLabel } from "@/lib/appTrigger";
+import { presentationModeLabel } from "@/lib/appTrigger";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAppSkills } from "@/hooks/useAppSkills";
@@ -336,9 +336,9 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
)}
- {/* 副标题:说明 · 触发方式 · 平台。描述可能为空,空时不留下孤零零的分隔点 */}
+ {/* 副标题:说明 · 展示方式 · 平台。描述可能为空,空时不留下孤零零的分隔点 */}
- {[appInfo.description, triggerLabel(appInfo.app_type, t), appInfo.platform]
+ {[appInfo.description, presentationModeLabel(appInfo.app_type, t), appInfo.platform]
.filter(Boolean)
.join(" · ")}
diff --git a/app/apps/components/subscription-agent/FeedItemsDialog.tsx b/app/apps/components/subscription-agent/FeedItemsDialog.tsx
deleted file mode 100644
index 3803342..0000000
--- a/app/apps/components/subscription-agent/FeedItemsDialog.tsx
+++ /dev/null
@@ -1,259 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { useTranslations } from "next-intl";
-import axios from "@/lib/axios";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Loader2, RefreshCw, ExternalLink, Download } from "lucide-react";
-import { toast } from "sonner";
-import type { Feed, FeedItem } from "@/types/subscription-agent";
-
-interface FeedItemsDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- feedIds: string[];
- appName?: string;
-}
-
-export function FeedItemsDialog({ open, onOpenChange, feedIds, appName }: FeedItemsDialogProps) {
- const t = useTranslations("workflow");
- const [feeds, setFeeds] = useState([]);
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(false);
- const [fetching, setFetching] = useState(false);
- const [selectedFeedId, setSelectedFeedId] = useState("all");
- const [page, setPage] = useState(1);
- const [total, setTotal] = useState(0);
- const pageSize = 20;
-
- const loadFeeds = useCallback(async () => {
- if (feedIds.length === 0) return;
-
- try {
- const feedPromises = feedIds.map((id) =>
- axios.get(`/api/v1/subscription-agent/feeds/${id}`)
- );
- const responses = await Promise.all(feedPromises);
- setFeeds(responses.map((r) => r.data));
- } catch (error: any) {
- console.error("Failed to load feeds:", error);
- }
- }, [feedIds]);
-
- const loadItems = useCallback(async () => {
- if (feedIds.length === 0) return;
-
- setLoading(true);
- try {
- const targetFeedIds = selectedFeedId === "all" ? feedIds : [selectedFeedId];
- const allItems: FeedItem[] = [];
- let totalCount = 0;
-
- for (const feedId of targetFeedIds) {
- const response = await axios.get(`/api/v1/subscription-agent/feeds/${feedId}/items`, {
- params: { page, pageSize },
- });
- allItems.push(...response.data.items);
- totalCount += response.data.pagination?.total || 0;
- }
-
- // 按发布时间排序
- allItems.sort(
- (a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()
- );
-
- setItems(allItems);
- setTotal(totalCount);
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("loadContentFailed"));
- } finally {
- setLoading(false);
- }
- }, [feedIds, selectedFeedId, page, pageSize, t]);
-
- const handleFetch = async () => {
- setFetching(true);
- try {
- await axios.post("/api/v1/subscription-agent/fetch", { feedIds });
- toast.success(t("fetchTaskStarted"));
- // 延迟刷新列表
- setTimeout(() => {
- loadItems();
- }, 2000);
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("triggerFetchFailed"));
- } finally {
- setFetching(false);
- }
- };
-
- useEffect(() => {
- if (open && feedIds.length > 0) {
- loadFeeds();
- loadItems();
- }
- }, [open, feedIds, loadFeeds, loadItems]);
-
- useEffect(() => {
- if (open) {
- setPage(1);
- }
- }, [selectedFeedId, open]);
-
- const totalPages = Math.ceil(total / pageSize);
-
- return (
-
- );
-}
diff --git a/app/apps/components/subscription-agent/ScheduleConfigSection.tsx b/app/apps/components/subscription-agent/ScheduleConfigSection.tsx
deleted file mode 100644
index 7f1ace9..0000000
--- a/app/apps/components/subscription-agent/ScheduleConfigSection.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-"use client";
-
-import { useTranslations } from "next-intl";
-import { Label } from "@/components/ui/label";
-import { Switch } from "@/components/ui/switch";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import type { ScheduleSettings } from "@/types/subscription-agent";
-
-interface ScheduleConfigSectionProps {
- schedule: ScheduleSettings;
- onChange: (schedule: ScheduleSettings) => void;
-}
-
-// Generate hour options (00-23)
-const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, "0"));
-
-// Generate minute options (00, 15, 30, 45)
-const minutes = ["00", "15", "30", "45"];
-
-export function ScheduleConfigSection({ schedule, onChange }: ScheduleConfigSectionProps) {
- const t = useTranslations("apps");
-
- const [hour, minute] = (schedule.time || "10:00").split(":");
-
- const handleTimeChange = (newHour: string, newMinute: string) => {
- onChange({
- ...schedule,
- time: `${newHour}:${newMinute}`,
- });
- };
-
- return (
-
-
-
- {/* Enable/Disable Switch */}
-
-
-
-
{t("enableScheduleDesc")}
-
-
- onChange({
- ...schedule,
- enabled: checked,
- })
- }
- />
-
-
- {/* Time and Report Type Selection - Only show when enabled */}
- {schedule.enabled && (
-
- {/* Schedule Time */}
-
-
-
-
-
- :
-
-
-
-
- {/* Report Type */}
-
-
-
-
-
-
- {/* Timezone - Read only */}
-
- {t("timezone")}:
- {schedule.timezone || "Asia/Shanghai"}
-
-
- )}
-
- );
-}
diff --git a/app/apps/components/subscription-agent/SummaryDetailDialog.tsx b/app/apps/components/subscription-agent/SummaryDetailDialog.tsx
deleted file mode 100644
index bdf356d..0000000
--- a/app/apps/components/subscription-agent/SummaryDetailDialog.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { useTranslations } from "next-intl";
-import axios from "@/lib/axios";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Loader2, RefreshCw, ExternalLink } from "lucide-react";
-import { toast } from "sonner";
-import ReactMarkdown from "react-markdown";
-import type { Summary } from "@/types/subscription-agent";
-
-// 后端可能返回的原始格式(与 Summary 类型有差异)
-interface RawSummary {
- id?: string;
- summaryId?: string;
- type: "daily" | "weekly";
- status: "pending" | "processing" | "completed" | "failed";
- period_start?: string;
- period_end?: string;
- period?: {
- start: string;
- end: string;
- };
- platform_filter?: "all" | "youtube" | "twitter";
- item_count?: number;
- itemCount?: number;
- summary_text?: string;
- summaryText?: string;
- highlights?: Summary["highlights"];
- llm_model?: string;
- llm_tokens_input?: number;
- llm_tokens_output?: number;
- triggered_by?: "scheduled" | "manual";
- triggeredBy?: "scheduled" | "manual";
- created_at?: string;
- createdAt?: string;
- updated_at?: string;
-}
-
-// 将原始数据转换为标准 Summary 格式
-function normalizeSummary(raw: RawSummary): Summary {
- return {
- id: raw.id || raw.summaryId || "",
- type: raw.type,
- status: raw.status,
- period_start: raw.period_start || raw.period?.start || "",
- period_end: raw.period_end || raw.period?.end || "",
- platform_filter: raw.platform_filter || "all",
- item_count: raw.item_count ?? raw.itemCount ?? 0,
- summary_text: raw.summary_text || raw.summaryText,
- highlights: raw.highlights,
- llm_model: raw.llm_model,
- llm_tokens_input: raw.llm_tokens_input,
- llm_tokens_output: raw.llm_tokens_output,
- triggered_by: raw.triggered_by || raw.triggeredBy || "manual",
- created_at: raw.created_at || raw.createdAt || new Date().toISOString(),
- updated_at: raw.updated_at,
- };
-}
-
-interface SummaryDetailDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- summaryId: string | null;
-}
-
-const typeColorMap: Record = {
- daily: "bg-blue-100 text-blue-800",
- weekly: "bg-purple-100 text-purple-800",
-};
-
-const statusColorMap: Record = {
- pending: "bg-yellow-100 text-yellow-800",
- processing: "bg-blue-100 text-blue-800",
- completed: "bg-green-100 text-green-800",
- failed: "bg-red-100 text-red-800",
-};
-
-export function SummaryDetailDialog({ open, onOpenChange, summaryId }: SummaryDetailDialogProps) {
- const t = useTranslations("workflow");
- const tc = useTranslations("common");
- const [summary, setSummary] = useState(null);
- const [loading, setLoading] = useState(false);
-
- const typeLabelMap: Record = {
- daily: t("dailyReport"),
- weekly: t("weeklyReport"),
- };
-
- const statusLabelMap: Record = {
- pending: t("pending"),
- processing: t("generating"),
- completed: t("completed"),
- failed: t("failed"),
- };
-
- const loadSummary = useCallback(async () => {
- if (!summaryId) return;
-
- setLoading(true);
- try {
- const response = await axios.get(
- `/api/v1/subscription-agent/summaries/${summaryId}`
- );
- setSummary(normalizeSummary(response.data));
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("loadReportFailed"));
- } finally {
- setLoading(false);
- }
- }, [summaryId, t]);
-
- useEffect(() => {
- if (open && summaryId) {
- loadSummary();
- }
- }, [open, summaryId, loadSummary]);
-
- useEffect(() => {
- if (!open) {
- setSummary(null);
- }
- }, [open]);
-
- // 自动轮询处理中的报告
- useEffect(() => {
- if (!open || !summary) return;
-
- if (summary.status === "pending" || summary.status === "processing") {
- const timer = setInterval(() => {
- loadSummary();
- }, 5000);
-
- return () => clearInterval(timer);
- }
- }, [open, summary, loadSummary]);
-
- return (
-
- );
-}
diff --git a/app/apps/components/subscription-agent/SummaryListDialog.tsx b/app/apps/components/subscription-agent/SummaryListDialog.tsx
deleted file mode 100644
index d144883..0000000
--- a/app/apps/components/subscription-agent/SummaryListDialog.tsx
+++ /dev/null
@@ -1,352 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { useTranslations } from "next-intl";
-import axios from "@/lib/axios";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Loader2, RefreshCw, Plus, ChevronDown } from "lucide-react";
-import { toast } from "sonner";
-import { SummaryDetailDialog } from "./SummaryDetailDialog";
-import type { Summary, SummaryListResponse } from "@/types/subscription-agent";
-
-interface SummaryListDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- feedIds: string[];
- appName?: string;
- topic?: string;
- webhookUrl?: string;
-}
-
-const typeColorMap: Record = {
- daily: "bg-blue-100 text-blue-800",
- weekly: "bg-purple-100 text-purple-800",
-};
-
-const statusColorMap: Record = {
- pending: "bg-yellow-100 text-yellow-800",
- processing: "bg-blue-100 text-blue-800",
- completed: "bg-green-100 text-green-800",
- failed: "bg-red-100 text-red-800",
-};
-
-export function SummaryListDialog({
- open,
- onOpenChange,
- feedIds,
- appName,
- topic,
- webhookUrl,
-}: SummaryListDialogProps) {
- const t = useTranslations("workflow");
- const [summaries, setSummaries] = useState([]);
- const [loading, setLoading] = useState(false);
- const [generating, setGenerating] = useState(false);
-
- const typeLabelMap: Record = {
- daily: t("dailyReport"),
- weekly: t("weeklyReport"),
- };
-
- const statusLabelMap: Record = {
- pending: t("pending"),
- processing: t("generating"),
- completed: t("completed"),
- failed: t("failed"),
- };
- const [typeFilter, setTypeFilter] = useState("all");
- const [statusFilter, setStatusFilter] = useState("all");
- const [page, setPage] = useState(1);
- const [total, setTotal] = useState(0);
- const pageSize = 20;
-
- // 详情弹窗
- const [detailOpen, setDetailOpen] = useState(false);
- const [selectedSummaryId, setSelectedSummaryId] = useState(null);
-
- const loadSummaries = useCallback(async () => {
- if (!feedIds || feedIds.length === 0) return;
-
- setLoading(true);
- try {
- const params: Record = {
- page: String(page),
- pageSize: String(pageSize),
- feedIds: feedIds.join(","),
- };
-
- if (typeFilter !== "all") {
- params.type = typeFilter;
- }
-
- if (statusFilter !== "all") {
- params.status = statusFilter;
- }
-
- const response = await axios.get(
- "/api/v1/subscription-agent/summaries",
- { params }
- );
-
- setSummaries(response.data.data || []);
- setTotal(response.data.pagination?.total || 0);
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("loadReportListFailed"));
- } finally {
- setLoading(false);
- }
- }, [feedIds, typeFilter, statusFilter, page, t]);
-
- const handleGenerateDaily = async () => {
- setGenerating(true);
- try {
- const response = await axios.post("/api/v1/subscription-agent/summaries/generate/today", {
- feedIds,
- topic: topic || undefined,
- webhook_url: webhookUrl || undefined,
- });
- toast.success(t("dailyReportStarted"));
- setSelectedSummaryId(response.data.summaryId);
- setDetailOpen(true);
- // 刷新列表
- loadSummaries();
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("generateDailyFailed"));
- } finally {
- setGenerating(false);
- }
- };
-
- const handleGenerateWeekly = async () => {
- setGenerating(true);
- try {
- const response = await axios.post("/api/v1/subscription-agent/summaries/generate/week", {
- feedIds,
- topic: topic || undefined,
- webhook_url: webhookUrl || undefined,
- });
- toast.success(t("weeklyReportStarted"));
- setSelectedSummaryId(response.data.summaryId);
- setDetailOpen(true);
- // 刷新列表
- loadSummaries();
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("generateWeeklyFailed"));
- } finally {
- setGenerating(false);
- }
- };
-
- const handleRowClick = (summary: Summary) => {
- setSelectedSummaryId(summary.id);
- setDetailOpen(true);
- };
-
- useEffect(() => {
- if (open && feedIds && feedIds.length > 0) {
- loadSummaries();
- }
- }, [open, feedIds, loadSummaries]);
-
- useEffect(() => {
- if (open) {
- setPage(1);
- }
- }, [typeFilter, statusFilter, open]);
-
- const totalPages = Math.ceil(total / pageSize);
-
- return (
- <>
-
-
-
- >
- );
-}
diff --git a/app/apps/page.tsx b/app/apps/page.tsx
index 7708a31..f1e027e 100644
--- a/app/apps/page.tsx
+++ b/app/apps/page.tsx
@@ -2,7 +2,7 @@
import { Fragment, useState, useEffect, useCallback, useMemo, useRef } from "react";
import { useTranslations } from "next-intl";
-import { triggerLabel } from "@/lib/appTrigger";
+import { presentationModeLabel } from "@/lib/appTrigger";
import { canEditApp } from "@/lib/appPermissions";
import AppAvatarPicker from "./components/AppAvatarPicker";
import AppAvatar from "./components/AppAvatar";
@@ -48,10 +48,9 @@ import {
Network,
FileText,
Code,
- List,
- X,
Sparkles,
LayoutGrid,
+ List,
} from "lucide-react";
import { appStatusBadge, type ReviewStatus } from "@/lib/reviewStatus";
import Link from "next/link";
@@ -64,17 +63,13 @@ import { checkSuperAdmin } from "@/lib/clientPermissions";
import WorkflowEditor from "./components/WorkflowEditor";
import { syncFormToWorkflow } from "@/lib/workflowUtils";
import { WorkflowConfig } from "@/types/workflow";
-import { FeedItemsDialog } from "./components/subscription-agent/FeedItemsDialog";
-import { SummaryListDialog } from "./components/subscription-agent/SummaryListDialog";
-import { ScheduleConfigSection } from "./components/subscription-agent/ScheduleConfigSection";
-import type { StreamFeedFormItem, ScheduleSettings } from "@/types/subscription-agent";
// 类型定义
interface App {
id: number;
name: string;
description: string;
- app_type: "Chat" | "Subscription" | "Email" | "Custom" | "Tool" | "Plugin";
+ app_type: "Chat" | "Custom";
platform: "Web" | "Wechat" | "Plugin" | "Feishu" | "iOS" | "Android";
avatar_url?: string | null;
user_id: number;
@@ -122,7 +117,7 @@ const platformIcons: Record = {
Android: Smartphone,
};
-// 触发方式与平台标签一律中性灰(Badge 的 secondary)。
+// 展示方式与平台标签一律中性灰(Badge 的 secondary)。
//
// 原先每个取值一种颜色:紫、绿、黄、蓝、翠、橙……一屏卡片下来像调色板,而这些
// 颜色不承载任何含义——"聊天"是紫的、"Web"也是紫的,读者得先学会一套配色表才
@@ -130,9 +125,6 @@ const platformIcons: Record = {
// 颜色留给它们才有对比度。详情页那两个标签一直是中性的 outline,这里跟它对齐。
-// 判断是否为订阅聚合应用
-const isStreamApp = (app: App) => app.app_type === "Subscription";
-
export default function AppsPage() {
const router = useRouter();
const { user } = useCurrentUser();
@@ -206,13 +198,6 @@ export default function AppsPage() {
const [embedDialogOpen, setEmbedDialogOpen] = useState(false);
const [embedAppId, setEmbedAppId] = useState("");
- // Stream Agent 相关状态
- const [feedItemsDialogOpen, setFeedItemsDialogOpen] = useState(false);
- const [summaryListDialogOpen, setSummaryListDialogOpen] = useState(false);
- const [selectedStreamApp, setSelectedStreamApp] = useState(null);
- const [streamFeeds, setStreamFeeds] = useState([]);
- const [newStreamUrl, setNewStreamUrl] = useState("");
- const [addingFeed, setAddingFeed] = useState(false);
const [viewMode, setViewMode] = useState<"table" | "grid">("grid");
const [autoSelectDatasets, setAutoSelectDatasets] = useState(true);
@@ -230,7 +215,7 @@ export default function AppsPage() {
const [formData, setFormData] = useState<{
name: string;
description: string;
- app_type: "Chat" | "Subscription" | "Email" | "Custom" | "Tool" | "Plugin";
+ app_type: "Chat" | "Custom";
platform: "Web" | "Wechat" | "Plugin" | "Feishu" | "iOS" | "Android";
// 空串 = 用户主动清空(后端据此落 NULL);null = 本来就没设过
avatar_url: string | null;
@@ -315,52 +300,6 @@ export default function AppsPage() {
}
}, [formData.platform, dialogOpen, loadWechatAgents]);
- // 添加 Stream URL(立即请求 API 创建订阅)
- const handleAddStreamUrl = useCallback(async () => {
- if (!newStreamUrl) return;
- setAddingFeed(true);
- try {
- const res = await axios.post("/api/v1/subscription-agent/feeds", {
- url: newStreamUrl,
- });
- const feedId = res.data.id;
- setStreamFeeds((prev) => {
- // 检查是否已存在,避免重复添加
- if (prev.some((f) => f.id === feedId)) {
- return prev;
- }
- return [
- ...prev,
- {
- id: feedId,
- url: res.data.source_url || res.data.url || newStreamUrl,
- name: res.data.name,
- platform: res.data.platform,
- },
- ];
- });
- setNewStreamUrl("");
- toast.success(t("subscriptionAdded", { name: res.data.name || newStreamUrl }));
- } catch (error: any) {
- toast.error(error.response?.data?.message || t("addSubscriptionFailed"));
- } finally {
- setAddingFeed(false);
- }
- }, [newStreamUrl]);
-
- // 删除订阅(从本地列表移除)
- const handleRemoveStreamFeed = useCallback((feedId: string) => {
- setStreamFeeds((prev) => prev.filter((f) => f.id !== feedId));
- }, []);
-
- // Default schedule settings
- const defaultScheduleSettings: ScheduleSettings = {
- enabled: false,
- time: "10:00",
- timezone: "Asia/Shanghai",
- report_type: "daily",
- };
-
// 打开创建对话框
const handleCreate = useCallback(() => {
setEditingApp(null);
@@ -376,8 +315,6 @@ export default function AppsPage() {
settings: {},
is_default: false,
});
- setStreamFeeds([]);
- setNewStreamUrl("");
setAutoSelectDatasets(true);
setDialogOpen(true);
}, []);
@@ -504,7 +441,7 @@ export default function AppsPage() {
}, [selectedTemplate, handleCreateQualityAgent]);
// 打开编辑对话框
- const handleEdit = useCallback(async (app: App) => {
+ const handleEdit = useCallback((app: App) => {
setEditingApp(app);
const datasetIds = app.dataset_ids || [];
setFormData({
@@ -522,34 +459,6 @@ export default function AppsPage() {
// 如果 dataset_ids 为空,则自动选择知识库为 true,否则为 false
setAutoSelectDatasets(datasetIds.length === 0);
- // 如果是 Stream 应用,加载已有的订阅信息
- if (isStreamApp(app) && app.settings?.stream_feed_ids?.length > 0) {
- // 优先从 settings.stream_feeds 读取(包含完整的 url 信息)
- if (app.settings?.stream_feeds?.length > 0) {
- setStreamFeeds(app.settings.stream_feeds);
- } else {
- // 兼容旧数据:从 API 获取
- try {
- const feedPromises = app.settings.stream_feed_ids.map((id: string) =>
- axios.get(`/api/v1/subscription-agent/feeds/${id}`)
- );
- const responses = await Promise.all(feedPromises);
- setStreamFeeds(
- responses.map((r) => ({
- id: r.data.id,
- url: r.data.source_url || r.data.url || "",
- name: r.data.name,
- platform: r.data.platform,
- }))
- );
- } catch {
- setStreamFeeds([]);
- }
- }
- } else {
- setStreamFeeds([]);
- }
- setNewStreamUrl("");
setDialogOpen(true);
}, []);
@@ -563,40 +472,14 @@ export default function AppsPage() {
try {
setSubmitting(true);
- let submitData: any;
-
- // Stream 应用特殊处理
- if (formData.app_type === "Subscription") {
- submitData = {
- ...formData,
- settings: {
- ...formData.settings,
- stream_feed_ids: streamFeeds.map((f) => f.id),
- // 保存完整的 feed 信息,因为外部 API 可能不返回 url
- stream_feeds: streamFeeds.map((f) => ({
- id: f.id,
- url: f.url,
- name: f.name,
- platform: f.platform,
- })),
- },
- };
- } else {
- // 同步表单数据到工作流配置
- const workflow = syncFormToWorkflow(
- { ...formData },
- editingApp?.settings?.workflow,
- datasets
- );
-
- submitData = {
- ...formData,
- settings: {
- ...formData.settings,
- workflow,
- },
- };
- }
+ const workflow = syncFormToWorkflow({ ...formData }, editingApp?.settings?.workflow, datasets);
+ const submitData = {
+ ...formData,
+ settings: {
+ ...formData.settings,
+ workflow,
+ },
+ };
if (editingApp) {
// 更新
@@ -621,7 +504,7 @@ export default function AppsPage() {
} finally {
setSubmitting(false);
}
- }, [formData, editingApp, loadApps, datasets, streamFeeds]);
+ }, [formData, editingApp, loadApps, datasets]);
// 提交应用审核(draft/rejected → pending_review;建即 draft,仅 owner 可测)
const handleSubmitAppReview = useCallback(
@@ -643,17 +526,6 @@ export default function AppsPage() {
if (!deletingApp) return;
try {
- // 如果是 Stream 应用,同时删除关联的所有订阅
- if (isStreamApp(deletingApp) && deletingApp.settings?.stream_feed_ids?.length > 0) {
- for (const feedId of deletingApp.settings.stream_feed_ids) {
- try {
- await axios.delete(`/api/v1/subscription-agent/feeds/${feedId}`);
- } catch (error) {
- console.error(`删除订阅 ${feedId} 失败:`, error);
- }
- }
- }
-
await axios.delete(`/api/v1/apps/${deletingApp.id}`);
toast.success(t("appDeleted"));
setDeleteConfirmOpen(false);
@@ -846,7 +718,7 @@ export default function AppsPage() {
- {triggerLabel(app.app_type, t)}
+ {presentationModeLabel(app.app_type, t)}
@@ -884,68 +756,24 @@ export default function AppsPage() {
- {/* Stream 应用特有操作 */}
- {isStreamApp(app) && (
- <>
-
-
-
-
-
- {t("viewContent")}
-
-
-
-
-
-
-
- {t("viewReport")}
-
-
- >
- )}
- {/* 非 Stream 应用显示工作流按钮 */}
- {!isStreamApp(app) && (
-
-
-
-
-
- {t("workflowConfig")}
-
-
- )}
+
+
+
+
+
+ {t("workflowConfig")}
+
+
{canSubmitAppReview(app) && (
@@ -1078,7 +906,7 @@ export default function AppsPage() {
) : null;
})()}
- {triggerLabel(app.app_type, t)}
+ {presentationModeLabel(app.app_type, t)}
@@ -1117,69 +945,25 @@ export default function AppsPage() {
{new Date(app.updated_at || app.created_at).toLocaleDateString()}
- {isStreamApp(app) && (
- <>
-
-
-
-
-
- {t("viewContent")}
-
-
-
-
-
-
-
- {t("viewReport")}
-
-
- >
- )}
- {!isStreamApp(app) && (
-
-
-
-
-
- {t("workflowConfig")}
-
-
- )}
+
+
+
+
+
+ {t("workflowConfig")}
+
+
{canSubmitAppReview(app) && (
@@ -1363,28 +1147,13 @@ export default function AppsPage() {
{t("setAsDefaultDesc")}
- {/*
- 触发方式 / 平台 / AI 模型 同一行。AI 模型对 Subscription、Custom
- 不适用,隐藏时列数跟着降到 2——否则会空出三分之一,看着像少了个字段。
- */}
-
+
@@ -1442,7 +1195,7 @@ export default function AppsPage() {
- {formData.app_type !== "Subscription" && formData.app_type !== "Custom" && (
+ {formData.app_type === "Chat" && (