Merge branch 'feature/monitoring-account-works'——监控账号详情页承载作品列表与统计 Drawer,blocked 同步自动重试
douyin-release-gate / verify (push) Failing after 3m11s
douyin-release-gate / verify (push) Failing after 3m11s
This commit is contained in:
@@ -1778,8 +1778,20 @@ func syncCreatorCompetitorWithClaim(ctx context.Context, store *creator.Store, p
|
||||
if !claimed {
|
||||
return creator.CollectionReport{}, creator.ErrConflict
|
||||
}
|
||||
// blocked(环境暂不可用)也必须排下一次重试:否则 next_sync_at 置空后定时获取对该账号永久失效。
|
||||
nextRetry := func() *time.Time {
|
||||
nextBase := now
|
||||
if competitor.NextSyncAt != nil {
|
||||
nextBase = competitor.NextSyncAt.UTC()
|
||||
}
|
||||
next := creator.NextFixedRun(nextBase, time.Now().UTC(), time.Duration(settings.NewWorkIntervalSeconds)*time.Second)
|
||||
if next.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &next
|
||||
}
|
||||
blocked := func(blockErr error) (creator.CollectionReport, error) {
|
||||
markErr := store.MarkCompetitorSync(ctx, competitorID, leaseToken, "blocked", "", blockErr.Error(), nil)
|
||||
markErr := store.MarkCompetitorSync(ctx, competitorID, leaseToken, "blocked", "", blockErr.Error(), nextRetry())
|
||||
return creator.CollectionReport{}, errors.Join(blockErr, markErr)
|
||||
}
|
||||
if competitor.Platform != creator.PlatformDouyin {
|
||||
@@ -1834,7 +1846,7 @@ func syncCreatorCompetitorWithClaim(ctx context.Context, store *creator.Store, p
|
||||
next := creator.NextFixedRun(nextBase, time.Now().UTC(), time.Duration(settings.NewWorkIntervalSeconds)*time.Second)
|
||||
status := "failed"
|
||||
if errors.Is(collectErr, creator.ErrUnavailable) || errors.Is(collectErr, creator.ErrConflict) {
|
||||
status, next = "blocked", time.Time{}
|
||||
status = "blocked"
|
||||
}
|
||||
var nextAt *time.Time
|
||||
if !next.IsZero() {
|
||||
@@ -1928,7 +1940,17 @@ func runCreatorScheduleOnce(ctx context.Context, store *creator.Store, phaseASto
|
||||
continue
|
||||
}
|
||||
if claimed {
|
||||
if markErr := store.MarkCompetitorSync(ctx, competitor.ID, leaseToken, "blocked", "", err.Error(), nil); markErr != nil {
|
||||
// blocked 也排下一次重试(按新作品间隔),避免无采集账号时永久卡死。
|
||||
nextBase := now
|
||||
if competitor.NextSyncAt != nil {
|
||||
nextBase = competitor.NextSyncAt.UTC()
|
||||
}
|
||||
next := creator.NextFixedRun(nextBase, now, time.Duration(settings.NewWorkIntervalSeconds)*time.Second)
|
||||
var nextAt *time.Time
|
||||
if !next.IsZero() {
|
||||
nextAt = &next
|
||||
}
|
||||
if markErr := store.MarkCompetitorSync(ctx, competitor.ID, leaseToken, "blocked", "", err.Error(), nextAt); markErr != nil {
|
||||
logrus.WithError(markErr).WithField("competitor_id", competitor.ID).Warn("creator competitor sync block update failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,21 @@ func TestCreatorPostgresContentAndWorkflow(t *testing.T) {
|
||||
if err := store.MarkCompetitorSync(ctx, competitor.ID, leaseToken, "idle", "", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// blocked 同步也必须排下一次重试:否则 next_sync_at 置空后定时获取对该账号永久失效。
|
||||
blockedToken, blockedClaimed, err := store.ClaimCompetitorSync(ctx, competitor.ID, true, dueNow)
|
||||
if err != nil || !blockedClaimed {
|
||||
t.Fatalf("claim competitor sync for blocked: err=%v claimed=%v", err, blockedClaimed)
|
||||
}
|
||||
blockedNext := dueNow.Add(2 * time.Hour)
|
||||
if err := store.MarkCompetitorSync(ctx, competitor.ID, blockedToken, "blocked", "", "runtime unavailable", &blockedNext); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if due, err := store.ListDueCompetitors(ctx, blockedNext); err != nil || len(due) != 1 || due[0].ID != competitor.ID {
|
||||
t.Fatalf("blocked competitor must reschedule: due=%+v err=%v", due, err)
|
||||
}
|
||||
if due, err := store.ListDueCompetitors(ctx, dueNow); err != nil || len(due) != 0 {
|
||||
t.Fatalf("blocked competitor must not fire before retry time: due=%+v err=%v", due, err)
|
||||
}
|
||||
work, inserted, err = store.UpsertWork(ctx, WorkInput{Platform: PlatformDouyin, WorkKey: workKey, SourceType: SourceCompetitor, SourceID: competitor.ID, Title: "", Body: "", PublishedAt: nil, Likes: nil, CommentsCount: nil, Shares: nil}, now)
|
||||
if err != nil || inserted || len(work.Sources) != 2 {
|
||||
t.Fatalf("work source association was not retained: work=%+v inserted=%v err=%v", work, inserted, err)
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
message,
|
||||
} from 'antd';
|
||||
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
|
||||
import { conflictMessage, dateTime, isCollectionAccount, platformLabel } from '@/utils/helpers';
|
||||
import { getList, remove, creatorAction, creatorUpdate, syncCompetitorAuto } from '@/services/api';
|
||||
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
|
||||
|
||||
interface CompetitorRow {
|
||||
id: string;
|
||||
@@ -88,14 +88,7 @@ export default function MonitoringAccountList() {
|
||||
async function syncCompetitor(competitor: CompetitorRow) {
|
||||
setActionBusy(competitor.id);
|
||||
try {
|
||||
const result = await getList({ resource: 'creator-accounts' });
|
||||
const account = (result.data ?? []).find(
|
||||
(candidate: any) => candidate.platform === competitor.platform && isCollectionAccount(candidate),
|
||||
);
|
||||
if (!account) {
|
||||
throw new Error(`没有可用于同步${platformLabel(competitor.platform)}监控账号的已登录自有账号。`);
|
||||
}
|
||||
await creatorAction(`/creator/competitors/${encodeURIComponent(competitor.id)}/sync`, { account_id: account.id });
|
||||
await syncCompetitorAuto(competitor);
|
||||
messageApi.success('监测账号同步任务已提交。');
|
||||
await load();
|
||||
} catch (actionError) {
|
||||
@@ -177,7 +170,10 @@ export default function MonitoringAccountList() {
|
||||
itemRender={(row) => {
|
||||
const meta = syncStatusMeta[row.sync_status ?? ''] || { label: row.sync_status || '—' };
|
||||
return (
|
||||
<div style={{ padding: '16px 0', borderBottom: '1px solid rgba(5,5,5,0.06)' }}>
|
||||
<div
|
||||
style={{ padding: '16px 0', borderBottom: '1px solid rgba(5,5,5,0.06)', cursor: 'pointer' }}
|
||||
onClick={() => history.push(`/accounts/monitoring/${row.id}`)}
|
||||
>
|
||||
<Flex justify="space-between" align="flex-start" gap={16}>
|
||||
<Flex align="flex-start" gap={12}>
|
||||
<Avatar size={44} src={row.avatar_url || undefined}>
|
||||
@@ -197,7 +193,7 @@ export default function MonitoringAccountList() {
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</Flex>
|
||||
<Space wrap>
|
||||
<Space wrap onClick={(e) => e.stopPropagation()}>
|
||||
<Button size="small" onClick={() => openTags(row)}>
|
||||
编辑标签
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
// 监控账号详情:形态对齐账号详情页(页头槽 + 卡片流)——
|
||||
// 页头 info 为账号标识(头像 + 昵称 + 同步状态 + 平台),actions 为返回/同步/暂停恢复;
|
||||
// 主体两张 Card:账号画像(Descriptions)与作品列表(Row/Col card 网格,分页)。
|
||||
// 点击作品弹出 Drawer 展示统计信息:作品详情 + 指标采集记录表。
|
||||
// 作品列表走 GET /creator/works?source_id=<competitor_id>,定时采集由后端调度器维护。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Empty,
|
||||
Flex,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { CommentOutlined, LikeOutlined, ReloadOutlined, ShareAltOutlined } from '@ant-design/icons';
|
||||
import { creatorAction, creatorGet, getOne, getList, syncCompetitorAuto } from '@/services/api';
|
||||
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
|
||||
import { usePageActions, usePageInfo } from '@/components/PageActions';
|
||||
|
||||
interface CompetitorDetail {
|
||||
id: string;
|
||||
platform: string;
|
||||
platform_account_key: string;
|
||||
unique_id?: string;
|
||||
nickname: string;
|
||||
avatar_url: string;
|
||||
homepage_url?: string;
|
||||
tags?: string[];
|
||||
enabled: boolean;
|
||||
follower_count?: number | null;
|
||||
following_count?: number | null;
|
||||
aweme_count?: number | null;
|
||||
sync_status?: string;
|
||||
sync_error?: string;
|
||||
last_sync_at?: string | null;
|
||||
next_sync_at?: string | null;
|
||||
}
|
||||
|
||||
const syncStatusMeta: Record<string, { color?: string; label: string }> = {
|
||||
idle: { color: 'success', label: '监控中' },
|
||||
running: { color: 'processing', label: '同步中' },
|
||||
paused: { label: '已暂停' },
|
||||
failed: { color: 'error', label: '同步失败' },
|
||||
blocked: { color: 'warning', label: '同步受阻' },
|
||||
};
|
||||
|
||||
function formatCount(value?: number | null): string {
|
||||
if (value === null || value === undefined) return '—';
|
||||
return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : `${value}`;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams<{ id: string }>();
|
||||
const [competitor, setCompetitor] = useState<CompetitorDetail | null>(null);
|
||||
const [pending, setPending] = useState(true);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [works, setWorks] = useState<any[]>([]);
|
||||
const [workPage, setWorkPage] = useState(1);
|
||||
const [workPageInfo, setWorkPageInfo] = useState({ total: 0, hasNext: false });
|
||||
const [worksPending, setWorksPending] = useState(true);
|
||||
const [worksError, setWorksError] = useState<any>(null);
|
||||
const [detailWork, setDetailWork] = useState<any>(null);
|
||||
const [workMetrics, setWorkMetrics] = useState<any[]>([]);
|
||||
const [metricsPending, setMetricsPending] = useState(false);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const loadCompetitor = useCallback(async () => {
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
setCompetitor(await getOne<CompetitorDetail>('creator-competitors', id));
|
||||
} catch (loadError) {
|
||||
setError(loadError);
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const loadWorks = useCallback(async () => {
|
||||
setWorksPending(true);
|
||||
setWorksError(null);
|
||||
try {
|
||||
const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters: { source_id: id } });
|
||||
setWorks(result.data);
|
||||
setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) });
|
||||
} catch (loadError) {
|
||||
setWorksError(loadError);
|
||||
} finally {
|
||||
setWorksPending(false);
|
||||
}
|
||||
}, [id, workPage]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCompetitor();
|
||||
}, [loadCompetitor]);
|
||||
useEffect(() => {
|
||||
loadWorks();
|
||||
}, [loadWorks]);
|
||||
|
||||
const syncMeta = syncStatusMeta[competitor?.sync_status ?? ''] || { label: competitor?.sync_status || '—' };
|
||||
|
||||
// 页头 content 行:左侧账号标识,右侧操作(返回/同步/暂停恢复),对齐账号详情页形态。
|
||||
usePageInfo(
|
||||
competitor ? (
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<Avatar size={28} src={competitor.avatar_url || undefined}>
|
||||
{(competitor.nickname || competitor.platform_account_key).slice(0, 1).toUpperCase()}
|
||||
</Avatar>
|
||||
<Typography.Title level={5} style={{ marginBottom: 0 }} ellipsis>
|
||||
{competitor.nickname || competitor.platform_account_key}
|
||||
</Typography.Title>
|
||||
<Tag color={syncMeta.color}>{syncMeta.label}</Tag>
|
||||
<Typography.Text type="secondary">
|
||||
{competitor.unique_id || competitor.platform_account_key} · {platformLabel(competitor.platform)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
) : null,
|
||||
[competitor?.id, competitor?.nickname, competitor?.sync_status],
|
||||
);
|
||||
|
||||
usePageActions(
|
||||
competitor ? (
|
||||
<Space>
|
||||
<Button onClick={() => history.push('/accounts/monitoring')}>返回监控账号</Button>
|
||||
<Button
|
||||
disabled={actionBusy || !competitor.enabled}
|
||||
onClick={async () => {
|
||||
setActionBusy(true);
|
||||
try {
|
||||
await syncCompetitorAuto(competitor);
|
||||
messageApi.success('监测账号同步任务已提交。');
|
||||
await loadCompetitor();
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '监测账号同步失败'));
|
||||
} finally {
|
||||
setActionBusy(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
立即同步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={actionBusy}
|
||||
onClick={async () => {
|
||||
setActionBusy(true);
|
||||
try {
|
||||
await creatorAction(
|
||||
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? 'pause' : 'resume'}`,
|
||||
);
|
||||
messageApi.success(competitor.enabled ? '监测账号已暂停。' : '监测账号已恢复。');
|
||||
await loadCompetitor();
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '监测账号状态更新失败'));
|
||||
} finally {
|
||||
setActionBusy(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{competitor.enabled ? '暂停监控' : '恢复监控'}
|
||||
</Button>
|
||||
</Space>
|
||||
) : null,
|
||||
[competitor?.id, competitor?.enabled],
|
||||
);
|
||||
|
||||
async function openWorkDetail(work: any) {
|
||||
setDetailWork(work);
|
||||
setWorkMetrics([]);
|
||||
setMetricsPending(true);
|
||||
try {
|
||||
setWorkMetrics((await creatorGet(`/creator/works/${encodeURIComponent(work.id)}/metrics`)) || []);
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '作品统计读取失败'));
|
||||
} finally {
|
||||
setMetricsPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (pending) return <Card loading />;
|
||||
if (error || !competitor) return <Alert type="error" showIcon title={error?.message || '监控账号不存在'} />;
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16}>
|
||||
{contextHolder}
|
||||
<Card title="账号画像">
|
||||
<Descriptions
|
||||
column={{ xs: 1, sm: 2, md: 3 }}
|
||||
size="small"
|
||||
items={[
|
||||
{ key: 'key', label: '平台账号', children: competitor.platform_account_key },
|
||||
{ key: 'uid', label: '抖音号', children: competitor.unique_id || '—' },
|
||||
{ key: 'follower', label: '粉丝', children: formatCount(competitor.follower_count) },
|
||||
{ key: 'following', label: '关注', children: formatCount(competitor.following_count) },
|
||||
{ key: 'aweme', label: '作品总数', children: formatCount(competitor.aweme_count) },
|
||||
{ key: 'tags', label: '标签', children: competitor.tags?.length ? competitor.tags.join('、') : '无' },
|
||||
{ key: 'lastSync', label: '上次同步', children: competitor.last_sync_at ? dateTime(competitor.last_sync_at) : '—' },
|
||||
{ key: 'nextSync', label: '下次自动同步', children: competitor.next_sync_at ? dateTime(competitor.next_sync_at) : '—' },
|
||||
{
|
||||
key: 'syncError',
|
||||
label: '同步异常',
|
||||
children: competitor.sync_error ? (
|
||||
<Typography.Text type="danger" style={{ fontSize: 12 }}>{competitor.sync_error}</Typography.Text>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title="作品列表"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} size="small" onClick={loadWorks} disabled={worksPending} aria-label="刷新作品" />
|
||||
}
|
||||
>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary">共 {workPageInfo.total} 个作品;作品由系统按采集间隔自动同步。</Typography.Text>
|
||||
</Flex>
|
||||
{worksError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
title={worksError.message}
|
||||
action={<Button size="small" onClick={loadWorks}>重试</Button>}
|
||||
/>
|
||||
) : null}
|
||||
<Spin spinning={worksPending}>
|
||||
{works.length ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{works.map((work) => (
|
||||
<Col key={work.id} xs={24} sm={12} lg={8} xl={6}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{ height: '100%' }}
|
||||
onClick={() => openWorkDetail(work)}
|
||||
cover={
|
||||
work.cover_url ? (
|
||||
<img src={work.cover_url} alt={work.title || '作品封面'} style={{ width: '100%', aspectRatio: '16 / 9', objectFit: 'cover' }} />
|
||||
) : (
|
||||
<Flex align="center" justify="center" style={{ aspectRatio: '16 / 9', background: '#f5f5f5' }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>无封面</Typography.Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Flex vertical gap={8} style={{ minWidth: 0 }}>
|
||||
<Typography.Paragraph strong ellipsis={{ rows: 2 }} style={{ marginBottom: 0 }}>
|
||||
{work.title || '无标题'}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{work.published_at ? dateTime(work.published_at) : '发布时间待核验'}
|
||||
</Typography.Text>
|
||||
<Flex gap={16}>
|
||||
<Typography.Text style={{ fontSize: 12 }}><LikeOutlined /> {work.likes ?? '—'}</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 12 }}><CommentOutlined /> {work.comments_count ?? '—'}</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 12 }}><ShareAltOutlined /> {work.shares ?? '—'}</Typography.Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description={worksPending ? '加载中…' : '暂无作品;新账号会在下一轮自动采集中拉取作品。'} />
|
||||
)}
|
||||
</Spin>
|
||||
{workPageInfo.total > works.length ? (
|
||||
<Flex justify="flex-end" style={{ marginTop: 12 }}>
|
||||
<Space>
|
||||
<Button size="small" onClick={() => setWorkPage((page) => Math.max(1, page - 1))} disabled={workPage === 1 || worksPending}>
|
||||
上一页
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setWorkPage((page) => page + 1)} disabled={!workPageInfo.hasNext || worksPending}>
|
||||
下一页
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Card>
|
||||
<Drawer open={Boolean(detailWork)} onClose={() => setDetailWork(null)} size="large" title="作品统计">
|
||||
{detailWork ? (
|
||||
<Flex vertical gap={12}>
|
||||
{detailWork.cover_url ? (
|
||||
<img src={detailWork.cover_url} alt="作品封面" style={{ maxHeight: 256, borderRadius: 4, border: '1px solid #f0f0f0' }} />
|
||||
) : null}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>ID:{detailWork.work_key || detailWork.id}</Typography.Text>
|
||||
<Typography.Paragraph strong style={{ marginBottom: 0 }}>{detailWork.title || '无标题'}</Typography.Paragraph>
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>
|
||||
{detailWork.body || '暂无正文'}
|
||||
</Typography.Paragraph>
|
||||
<Space wrap>
|
||||
<Typography.Text type="secondary">发布:{detailWork.published_at ? dateTime(detailWork.published_at) : '—'}</Typography.Text>
|
||||
{detailWork.original_url ? (
|
||||
<Typography.Link href={detailWork.original_url} target="_blank">打开原文</Typography.Link>
|
||||
) : null}
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="collected_at"
|
||||
size="small"
|
||||
loading={metricsPending}
|
||||
pagination={false}
|
||||
dataSource={workMetrics}
|
||||
columns={[
|
||||
{ title: '采集时间', dataIndex: 'collected_at', render: (value: string) => dateTime(value) },
|
||||
{ title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' },
|
||||
{ title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' },
|
||||
{ title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' },
|
||||
]}
|
||||
locale={{ emptyText: '暂无指标记录。' }}
|
||||
/>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Drawer>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 领域 API 层:资源路径与语义逐一对照 web.archived/src/shared/api/dataProvider.js。
|
||||
// 全部经由 requestErrorConfig.request(Basic Auth + 401 全局登出)。
|
||||
import { AUTH_STORAGE_KEY, RequestError, jsonOptions, request, unauthorized } from '@/requestErrorConfig';
|
||||
import { isCollectionAccount, platformLabel } from '@/utils/helpers';
|
||||
|
||||
export const resourcePaths = {
|
||||
browsers: '/browsers',
|
||||
@@ -149,6 +150,18 @@ export function creatorGet(path: string) {
|
||||
return request(path);
|
||||
}
|
||||
|
||||
// 监控账号同步:自动挑选可用的自有采集账号(登录且授权)作为执行身份。
|
||||
export async function syncCompetitorAuto(competitor: { id: string; platform: string }): Promise<void> {
|
||||
const result = await getList({ resource: 'creator-accounts' });
|
||||
const account = (result.data ?? []).find(
|
||||
(candidate: any) => candidate.platform === competitor.platform && isCollectionAccount(candidate),
|
||||
);
|
||||
if (!account) {
|
||||
throw new Error(`没有可用于同步${platformLabel(competitor.platform)}监控账号的已登录自有账号。`);
|
||||
}
|
||||
await creatorAction(`/creator/competitors/${encodeURIComponent(competitor.id)}/sync`, { account_id: account.id });
|
||||
}
|
||||
|
||||
export function creatorUpdate(path: string, data: unknown) {
|
||||
return request(path, jsonOptions('PUT', data));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s
|
||||
{ test: (p) => p === '/accounts/import', title: '账号导入', subtitle: '管理分享链接解析任务,解析成功后自动加入监控账号。' },
|
||||
{ test: (p) => p === '/accounts', title: '我的账号', subtitle: '创建和管理自己维护的账号。' },
|
||||
{ test: (p) => p === '/accounts/monitoring', title: '监控账号', subtitle: '管理需要持续跟踪的竞品账号及采集状态。' },
|
||||
{ test: (p) => /^\/accounts\/monitoring\/.+/.test(p), title: '监控账号详情', subtitle: '查看监控账号画像与作品列表,点击作品可追溯指标统计。' },
|
||||
{ test: (p) => p === '/accounts/new', title: '创建社媒账号', subtitle: '创建账号后,再在编辑页配置登录身份与账号策略。' },
|
||||
{ test: (p) => /\/edit$/.test(p) && p.startsWith('/accounts/'), title: '编辑社媒账号', subtitle: '维护账号资料、登录核验与自动响应策略。' },
|
||||
{ test: (p) => p.startsWith('/accounts/'), title: '账号详情', subtitle: '查看账号状态、登录身份与运行环境绑定。' },
|
||||
|
||||
Reference in New Issue
Block a user