refactor(platform): 平台收敛为抖音——移除小红书全链路,监控账号新增画像字段与卡片列表

- 移除小红书:internal/platform/xiaohongshu、controlplane XHS 分支与路由、
  browser_gateway XHS 平台模块与处理器、前端平台选项;migration 038 清理
  XHS 数据并将 creator_* 平台 CHECK 收紧为 douyin
- creator_competitor 新增 follower_count/following_count/aweme_count(仅
  接口定义返回,暂不采集回填);列表接口返回 work_count 与
  latest_published_at(creator_work_source 聚合 MAX(published_at))
- 导入页账号名称列展示作者头像(share job 列表带出 avatar_url)
- 监控账号页改 antd Listy 卡片列表(头像+粉丝/关注/作品/最近发布/同步状态);
  AccountManagementList 收敛为 owned 专用
- 环境模块清理 browser_version 遗留:migration 034 补齐删列,修复 store.go
  中 hub 别名悬空引用,测试 fixture 对齐新 schema
This commit is contained in:
2026-09-24 19:30:28 +08:00
parent 08e813c0ca
commit 6cd4cdc57f
43 changed files with 690 additions and 2665 deletions
@@ -1,3 +1,5 @@
// 我的账号列表(owned):语义对齐 web.archived AccountManagementList(mode="owned")。
// 监控账号列表已拆分至 components/accounts/MonitoringAccountList.tsx(card list)。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import {
@@ -6,7 +8,6 @@ import {
Button,
Card,
Form,
Input,
Modal,
Popconfirm,
Select,
@@ -16,12 +17,12 @@ import {
Typography,
message,
} from 'antd';
import { PlusOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
import { conflictMessage, isCollectionAccount, platformLabel } from '@/utils/helpers';
import { getList, remove, creatorUpdate } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
import { usePageActions } from '@/components/PageActions';
// 语义对齐 web.archived/src/features/accounts/AccountsPage.jsx 的 AccountManagementList(mode: owned/monitoring)。
// 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。
interface Row {
id: string;
@@ -29,15 +30,11 @@ interface Row {
platform: string;
platform_account_key: string;
tags?: string[];
source_type: 'owned' | 'monitoring';
runtime_status?: string;
authorization_status?: string;
enabled?: boolean;
nickname?: string;
}
export default function AccountManagementList({ mode }: { mode: 'owned' | 'monitoring' }) {
const isMonitoring = mode === 'monitoring';
export default function AccountManagementList() {
const [rows, setRows] = useState<Row[]>([]);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
@@ -51,13 +48,11 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
setPending(true);
setError(null);
try {
const resource = isMonitoring ? 'creator-competitors' : 'accounts';
const result = await getList({ resource: resource as any });
const result = await getList({ resource: 'accounts' });
setRows(
(result.data ?? []).map((account: any) => ({
...account,
name: isMonitoring ? account.nickname || account.platform_account_key : account.name,
source_type: isMonitoring ? 'monitoring' : 'owned',
name: account.name,
tags: account.tags ?? [],
})),
);
@@ -66,56 +61,20 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
} finally {
setPending(false);
}
}, [isMonitoring]);
}, []);
useEffect(() => {
load();
}, [load]);
async function syncCompetitor(competitor: Row) {
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 });
messageApi.success('监测账号同步任务已提交。');
await load();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号同步失败'));
} finally {
setActionBusy('');
}
}
async function toggleCompetitor(competitor: Row) {
setActionBusy(competitor.id);
try {
await creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? 'pause' : 'resume'}`,
);
messageApi.success(competitor.enabled ? '监测账号已暂停。' : '监测账号已恢复。');
await load();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号状态更新失败'));
} finally {
setActionBusy('');
}
}
async function deleteAccount(row: Row) {
const label = row.source_type === 'owned' ? '自有账号' : '监测账号';
setActionBusy(`${row.source_type}:${row.id}`);
setActionBusy(row.id);
try {
await remove(row.source_type === 'owned' ? 'accounts' : 'creator-competitors', row.id);
messageApi.success(`${label}已删除。`);
await remove('accounts', row.id);
messageApi.success('自有账号已删除。');
await load();
} catch (deleteError) {
messageApi.error(conflictMessage(deleteError, `${label}删除失败`));
messageApi.error(conflictMessage(deleteError, '自有账号删除失败'));
} finally {
setActionBusy('');
}
@@ -131,9 +90,7 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
const { tags } = await tagForm.validateFields();
setTagBusy(true);
try {
await (tagTarget.source_type === 'owned'
? creatorUpdate(`/creator/accounts/${encodeURIComponent(tagTarget.id)}/tags`, { tags })
: creatorUpdate(`/creator/competitors/${encodeURIComponent(tagTarget.id)}`, { tags }));
await creatorUpdate(`/creator/accounts/${encodeURIComponent(tagTarget.id)}/tags`, { tags });
setRows((current) => current.map((row) => (row.id === tagTarget.id ? { ...row, tags } : row)));
setTagTarget(null);
messageApi.success('标签已保存。');
@@ -150,18 +107,14 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
dataIndex: 'name',
render: (_, account) => (
<div>
{account.source_type === 'owned' ? (
<Typography.Link onClick={() => history.push(`/accounts/${account.id}`)}>{account.name}</Typography.Link>
) : (
<span>{account.name}</span>
)}
<Typography.Link onClick={() => history.push(`/accounts/${account.id}`)}>{account.name}</Typography.Link>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }} ellipsis>
{account.platform_account_key}
</Typography.Paragraph>
</div>
),
},
{ title: '平台', dataIndex: 'platform', render: (value: string) => platformLabel(value) },
{ title: '平台', dataIndex: 'platform', render: () => '抖音' },
{
title: '标签',
dataIndex: 'tags',
@@ -170,17 +123,14 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
{
title: '状态',
dataIndex: 'status',
render: (_, account) =>
account.source_type === 'owned' ? (
<Flex>
<Tag color={account.runtime_status === 'active' ? 'success' : 'default'}>
{account.runtime_status === 'active' ? '启用' : '暂停'}
</Tag>
<Typography.Text type="secondary">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Typography.Text>
</Flex>
) : (
<Tag color={account.enabled ? 'success' : 'default'}>{account.enabled ? '监控中' : '已暂停'}</Tag>
),
render: (_, account) => (
<Flex>
<Tag color={account.runtime_status === 'active' ? 'success' : 'default'}>
{account.runtime_status === 'active' ? '启用' : '暂停'}
</Tag>
<Typography.Text type="secondary">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Typography.Text>
</Flex>
),
},
{
title: '操作',
@@ -191,31 +141,18 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
<Button size="small" onClick={() => openTags(account)}>
编辑标签
</Button>
{account.source_type === 'owned' ? (
<>
<Button size="small" onClick={() => history.push(`/accounts/${account.id}/edit`)}>
编辑账号
</Button>
<Button size="small" onClick={() => history.push(`/accounts/${account.id}`)}>
查看账号
</Button>
</>
) : (
<>
<Button size="small" disabled={actionBusy === account.id} onClick={() => syncCompetitor(account)}>
同步
</Button>
<Button size="small" disabled={actionBusy === account.id} onClick={() => toggleCompetitor(account)}>
{account.enabled ? '暂停' : '恢复'}
</Button>
</>
)}
<Button size="small" onClick={() => history.push(`/accounts/${account.id}/edit`)}>
编辑账号
</Button>
<Button size="small" onClick={() => history.push(`/accounts/${account.id}`)}>
查看账号
</Button>
<Popconfirm
title={`确定删除${account.source_type === 'owned' ? '自有账号' : '监测账号'}“${account.name}”吗?相关专属资源也会被清理。`}
title={`确定删除自有账号“${account.name}”吗?相关专属资源也会被清理。`}
onConfirm={() => deleteAccount(account)}
disabled={actionBusy === `${account.source_type}:${account.id}`}
disabled={actionBusy === account.id}
>
<Button size="small" danger disabled={actionBusy === `${account.source_type}:${account.id}`}>
<Button size="small" danger disabled={actionBusy === account.id}>
删除
</Button>
</Popconfirm>
@@ -224,35 +161,28 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit
},
];
const primaryAction = isMonitoring ? (
<Button type="primary" icon={<CloudUploadOutlined />} onClick={() => history.push('/accounts/import')}>
导入竞品账号
</Button>
) : (
usePageActions(
<Button type="primary" icon={<PlusOutlined />} onClick={() => history.push('/accounts/new')}>
创建账号
</Button>
</Button>,
);
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">
共 {rows.length} 个{isMonitoring ? '监控' : '自有'}账号
</Typography.Text>
{primaryAction}
<Typography.Text type="secondary">共 {rows.length} 个自有账号</Typography.Text>
</Flex>
{error ? (
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
) : null}
<Table
rowKey={(row) => `${row.source_type}:${row.id}`}
rowKey="id"
columns={columns}
dataSource={rows}
loading={pending}
pagination={false}
locale={{ emptyText: isMonitoring ? '暂无监控账号。' : '暂无自有账号。' }}
locale={{ emptyText: '暂无自有账号。' }}
/>
<Modal
open={Boolean(tagTarget)}
@@ -0,0 +1,270 @@
// 监控账号列表(monitoring):一行一个账号的 card list,对齐 pro table-list 的卡片列表形态。
// 展示作者头像与粉丝/关注/作品/最近发布等账号画像属性(migration 038 字段 + work 聚合)。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import {
Alert,
Avatar,
Button,
Card,
Descriptions,
Empty,
Flex,
Form,
Listy,
Modal,
Popconfirm,
Select,
Space,
Spin,
Tag,
Typography,
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';
interface CompetitorRow {
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;
work_count?: number;
latest_published_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 MonitoringAccountList() {
const [rows, setRows] = useState<CompetitorRow[]>([]);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [tagTarget, setTagTarget] = useState<CompetitorRow | null>(null);
const [tagForm] = Form.useForm<{ tags: string[] }>();
const [tagBusy, setTagBusy] = useState(false);
const [actionBusy, setActionBusy] = useState('');
const [messageApi, contextHolder] = message.useMessage();
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const result = await getList({ resource: 'creator-competitors' });
setRows(result.data ?? []);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
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 });
messageApi.success('监测账号同步任务已提交。');
await load();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号同步失败'));
} finally {
setActionBusy('');
}
}
async function toggleCompetitor(competitor: CompetitorRow) {
setActionBusy(competitor.id);
try {
await creatorAction(
`/creator/competitors/${encodeURIComponent(competitor.id)}/${competitor.enabled ? 'pause' : 'resume'}`,
);
messageApi.success(competitor.enabled ? '监测账号已暂停。' : '监测账号已恢复。');
await load();
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '监测账号状态更新失败'));
} finally {
setActionBusy('');
}
}
async function deleteAccount(row: CompetitorRow) {
setActionBusy(row.id);
try {
await remove('creator-competitors', row.id);
messageApi.success('监测账号已删除。');
await load();
} catch (deleteError) {
messageApi.error(conflictMessage(deleteError, '监测账号删除失败'));
} finally {
setActionBusy('');
}
}
function openTags(row: CompetitorRow) {
setTagTarget(row);
tagForm.setFieldsValue({ tags: Array.isArray(row.tags) ? row.tags : [] });
}
async function saveTags() {
if (!tagTarget) return;
const { tags } = await tagForm.validateFields();
setTagBusy(true);
try {
await creatorUpdate(`/creator/competitors/${encodeURIComponent(tagTarget.id)}`, { tags });
setRows((current) => current.map((row) => (row.id === tagTarget.id ? { ...row, tags } : row)));
setTagTarget(null);
messageApi.success('标签已保存。');
} catch (saveError) {
messageApi.error(conflictMessage(saveError, '标签保存失败'));
} finally {
setTagBusy(false);
}
}
const syncMeta = syncStatusMeta[tagTarget?.sync_status ?? ''] || { label: tagTarget?.sync_status || '—' };
return (
<Card variant="outlined">
{contextHolder}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<Typography.Text type="secondary">共 {rows.length} 个监控账号</Typography.Text>
<Space>
<Button icon={<ReloadOutlined />} onClick={load} disabled={pending} aria-label="刷新" />
<Button type="primary" icon={<CloudUploadOutlined />} onClick={() => history.push('/accounts/import')}>
导入竞品账号
</Button>
</Space>
</Flex>
{error ? (
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
) : null}
<Listy
rowKey={(row) => row.id}
items={rows}
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)' }}>
<Flex justify="space-between" align="flex-start" gap={16}>
<Flex align="flex-start" gap={12}>
<Avatar size={44} src={row.avatar_url || undefined}>
{(row.nickname || row.platform_account_key).slice(0, 1).toUpperCase()}
</Avatar>
<div>
<Space wrap size={8}>
<Typography.Text strong>{row.nickname || row.platform_account_key}</Typography.Text>
<Tag>{platformLabel(row.platform)}</Tag>
<Tag color={meta.color}>{meta.label}</Tag>
{(row.tags ?? []).map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</Space>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }} ellipsis>
{row.platform_account_key}
</Typography.Paragraph>
</div>
</Flex>
<Space wrap>
<Button size="small" onClick={() => openTags(row)}>
编辑标签
</Button>
<Button size="small" disabled={actionBusy === row.id} onClick={() => syncCompetitor(row)}>
同步
</Button>
<Button size="small" disabled={actionBusy === row.id} onClick={() => toggleCompetitor(row)}>
{row.enabled ? '暂停' : '恢复'}
</Button>
<Popconfirm
title={`确定删除监测账号“${row.nickname || row.platform_account_key}”吗?相关专属资源也会被清理。`}
onConfirm={() => deleteAccount(row)}
disabled={actionBusy === row.id}
>
<Button size="small" danger disabled={actionBusy === row.id}>
删除
</Button>
</Popconfirm>
</Space>
</Flex>
<Descriptions
style={{ marginTop: 12 }}
size="small"
column={{ xs: 2, sm: 3, md: 6 }}
items={[
{ key: 'follower', label: '粉丝', children: formatCount(row.follower_count) },
{ key: 'following', label: '关注', children: formatCount(row.following_count) },
{ key: 'works', label: '作品', children: formatCount(row.work_count ?? row.aweme_count ?? null) },
{ key: 'published', label: '最近发布', children: row.latest_published_at ? dateTime(row.latest_published_at) : '—' },
{ key: 'lastSync', label: '上次同步', children: row.last_sync_at ? dateTime(row.last_sync_at) : '—' },
{
key: 'syncError',
label: '同步异常',
children: row.sync_error ? (
<Typography.Text type="danger" style={{ fontSize: 12 }}>{row.sync_error}</Typography.Text>
) : (
'—'
),
},
]}
/>
</div>
);
}}
/>
{pending ? (
<div style={{ textAlign: 'center', padding: '32px 0' }}>
<Spin />
</div>
) : null}
{!pending && !error && rows.length === 0 ? <Empty description="暂无监控账号。" /> : null}
<Modal
open={Boolean(tagTarget)}
title={`编辑标签 · ${tagTarget?.nickname || tagTarget?.platform_account_key || '账号'}`}
onCancel={() => {
if (!tagBusy) setTagTarget(null);
}}
onOk={saveTags}
confirmLoading={tagBusy}
okText={tagBusy ? '保存中…' : '保存标签'}
>
<Form form={tagForm} layout="vertical">
<Form.Item name="tags" label="标签" extra="回车或逗号确认,最多 20 个标签,每个最多 32 个字符。">
<Select mode="tags" tokenSeparators={[',', ',']} placeholder="如:重点监控" open={false} />
</Form.Item>
</Form>
</Modal>
</Card>
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ import {
import { creatorAction, creatorCreate, creatorGet, creatorRequest, creatorUpdate, getList } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
const platformLabelMap: Record<string, string> = { douyin: '抖音', xiaohongshu: '小红书' };
const platformLabelMap: Record<string, string> = { douyin: '抖音' };
const eventTypeOptions = [
{ value: 'comment', label: '评论' },
{ value: 'like', label: '点赞' },
+14 -4
View File
@@ -2,8 +2,10 @@
import { useCallback, useEffect, useState } from 'react';
import {
Alert,
Avatar,
Button,
Card,
Flex,
Form,
Input,
Modal,
@@ -29,6 +31,7 @@ interface Job {
failure_reason?: string;
created_at?: string;
author_name?: string;
author_avatar_url?: string;
competitor_id?: string;
}
@@ -128,8 +131,15 @@ export default function Page() {
</Typography.Text>
);
}
// 名称来自 competitor_id 关联的监控账号(creator_competitor),点击跳转竞品作者详情。
const inner = <Typography.Text strong>{value}</Typography.Text>;
// 名称/头像来自 competitor_id 关联的监控账号(creator_competitor),点击跳转竞品作者详情。
const inner = (
<Flex align="center" gap={8}>
<Avatar size={24} src={job?.author_avatar_url || undefined}>
{value.slice(0, 1).toUpperCase()}
</Avatar>
<Typography.Text strong>{value}</Typography.Text>
</Flex>
);
return job?.competitor_id ? (
<Typography.Link href={`/creator/competitors/${job.competitor_id}`}>{inner}</Typography.Link>
) : (
@@ -199,13 +209,13 @@ export default function Page() {
<Modal open={createOpen} title="创建账号导入任务" onCancel={closeCreate} onOk={createJob} confirmLoading={busy} okText={busy ? '加入中…' : '加入解析队列'} width={560}>
<Form form={form} layout="vertical">
<Form.Item name="shareText" label="分享内容或链接" required extra="可直接粘贴平台分享文案,系统会自动提取其中的链接并识别作者。">
<Input.TextArea rows={4} placeholder="粘贴抖音或小红书的分享内容" />
<Input.TextArea rows={4} placeholder="粘贴抖音的分享内容" />
</Form.Item>
{shareText ? (
extractedShareURL ? (
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
已提取链接:{extractedShareURL}
{sharePlatform ? ` · ${sharePlatform === 'douyin' ? '抖音' : '小红书'}` : ' · 待识别平台'}
{sharePlatform ? ' · 抖音' : ' · 待识别平台'}
</Typography.Paragraph>
) : (
<Typography.Paragraph type="danger" style={{ fontSize: 12 }}>未找到可用的 http(s) 分享链接。</Typography.Paragraph>
+2 -2
View File
@@ -1,6 +1,6 @@
// 我的账号列表:语义对齐 web.archived AccountManagementList(mode="owned")。
// 我的账号列表(owned):语义对齐 web.archived AccountManagementList(mode="owned")。
import AccountManagementList from '@/components/accounts/AccountManagementList';
export default function Page() {
return <AccountManagementList mode="owned" />;
return <AccountManagementList />;
}
+3 -3
View File
@@ -1,6 +1,6 @@
// 监控账号列表:语义对齐 web.archived AccountManagementList(mode="monitoring")。
import AccountManagementList from '@/components/accounts/AccountManagementList';
// 监控账号列表:一行一个账号的 card list(含头像与粉丝/关注/作品/最近发布画像)。
import MonitoringAccountList from '@/components/accounts/MonitoringAccountList';
export default function Page() {
return <AccountManagementList mode="monitoring" />;
return <MonitoringAccountList />;
}
-4
View File
@@ -15,9 +15,6 @@ export function conflictMessage(error: any, conflictHint?: string): string {
export const platforms = [
{ value: 'douyin', label: '抖音' },
{ value: 'xiaohongshu', label: '小红书' },
{ value: 'wechat-official', label: '公众号' },
{ value: 'kuaishou', label: '快手' },
];
export function platformLabel(value?: string): string {
@@ -39,7 +36,6 @@ export function platformForShareURL(value: string): string {
try {
const hostname = new URL(value).hostname.toLowerCase();
if (['www.douyin.com', 'v.douyin.com'].includes(hostname)) return 'douyin';
if (['www.xiaohongshu.com', 'xhslink.com', 'www.xhslink.com'].includes(hostname)) return 'xiaohongshu';
} catch {
return '';
}