refactor(web): 删除前端空转链路(任务中心/草稿/attempt/运营工作台/素材面板)

- 删页面目录 tasks/ drafts/ attempts/ creator/workbench/
- 竞品页移除素材处理 Drawer 与素材操作
- metadata 菜单与页头规则同步收敛
- api.ts/listQuery.ts 移除对应 resource 与函数
- 固定表头测试文件清单同步
This commit is contained in:
2026-09-28 15:54:32 +08:00
parent 35f2ebd44c
commit c2bccba352
11 changed files with 11 additions and 2084 deletions
+5 -54
View File
@@ -6,8 +6,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Line } from '@ant-design/charts';
import { Alert, App, Button, Card, Descriptions, Flex, Form, Input, Modal, Space, Tag, Typography } from 'antd';
import { accountAction, createDraft, getOne, getList, creatorGet, creatorAction } from '@/services/api';
import { Alert, App, Button, Card, Descriptions, Flex, Modal, Space, Tag, Typography } from 'antd';
import { accountAction, getOne, getList, creatorGet, creatorAction } from '@/services/api';
import { accountReadiness, conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
import { usePageActions, usePageInfo } from '@/components/PageActions';
import WorksGallery from '@/components/creator/WorksGallery';
@@ -65,13 +65,10 @@ export default function Page() {
const [account, setAccount] = useState<any>(null);
const [binding, setBinding] = useState<any>(null);
const [browsersError, setBrowsersError] = useState<any>(null);
const [drafts, setDrafts] = useState<any[]>([]);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [pauseOpen, setPauseOpen] = useState(false);
const [actionBusy, setActionBusy] = useState(false);
const [draftForm] = Form.useForm<{ content: string }>();
const [draftBusy, setDraftBusy] = useState(false);
const [collectionStatus, setCollectionStatus] = useState<any>(null);
const [accountMetrics, setAccountMetrics] = useState<any[]>([]);
const { message: messageApi } = App.useApp();
@@ -124,12 +121,6 @@ export default function Page() {
setBrowsersError(browsersResult.reason);
setBinding(undefined);
}
try {
const draftResult = await getList({ resource: 'drafts', filters: { account_id: id } });
setDrafts(draftResult.data ?? []);
} catch {
setDrafts([]);
}
// 监控数据:采集失败不阻塞页面主体,采集状态为空时按待采集展示。
try {
setCollectionStatus(await creatorGet(`/creator/accounts/${encodeURIComponent(id)}/collection-status`));
@@ -184,7 +175,7 @@ export default function Page() {
try {
await accountAction(account.id, action);
messageApi.success(
action === 'pause' ? '账号已暂停;既有 hold 任务需后续逐条核验。' : '账号已恢复;既有 hold 任务没有自动恢复。',
action === 'pause' ? '账号已暂停,运行环境已停止。' : '账号已恢复。',
);
await loadAll();
} catch (reason) {
@@ -194,22 +185,6 @@ export default function Page() {
}
}
async function onCreateDraft() {
if (!readiness?.ready) return;
const { content } = await draftForm.validateFields();
setDraftBusy(true);
try {
const draft = await createDraft(account.id, content);
await loadAll();
draftForm.resetFields();
messageApi.success(`草稿版本 ${draft.version} 已创建,请进入只读快照核对。`);
} catch (reason) {
messageApi.error(conflictMessage(reason));
} finally {
setDraftBusy(false);
}
}
if (pending) return <Card loading />;
if (error || !account) return <Alert type="error" showIcon title={error?.message || '账号不存在'} />;
@@ -256,7 +231,7 @@ export default function Page() {
) : null}
</Descriptions.Item>
</Descriptions>
<Alert type="info" showIcon title="暂停会把待领取任务置为 hold;恢复账号只恢复账号可用性,不会自动恢复既有 hold。" />
<Alert type="info" showIcon title="暂停账号会同时停止其运行环境;恢复账号后可在运行环境页重新启动。" />
</Flex>
</Card>
<Card title="账号数据">
@@ -306,35 +281,11 @@ export default function Page() {
) : (
<Flex vertical gap={12}>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。
尚未绑定运行环境。账号创建时若绑定失败,可联系管理员排查后重试。
</Typography.Paragraph>
<Typography.Link onClick={() => history.push('/browsers')}>前往运行环境</Typography.Link>
</Flex>
)}
</Card>
<Card title="文本草稿">
<Flex vertical gap={16}>
<Typography.Text type="secondary">仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。</Typography.Text>
<Form form={draftForm} layout="vertical">
<Form.Item name="content" rules={[{ required: true, message: '请输入草稿内容' }]}>
<Input.TextArea rows={4} placeholder="输入要发布的文本草稿" />
</Form.Item>
<Button type="primary" disabled={!readiness?.ready} loading={draftBusy} onClick={onCreateDraft}>
创建草稿
</Button>
</Form>
{drafts.length ? (
<Flex vertical gap={8}>
{drafts.map((draft: any) => (
<Flex key={draft.id} justify="space-between" align="center" wrap="wrap" gap={8}>
<Typography.Text ellipsis style={{ maxWidth: '70%' }}>{draft.content}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>v{draft.version}</Typography.Text>
</Flex>
))}
</Flex>
) : null}
</Flex>
</Card>
<Modal
open={pauseOpen}
title="暂停账号"
-68
View File
@@ -1,68 +0,0 @@
// Attempt 详情:语义对齐 web.archived TracePages.jsx 的 AttemptDetail。
import { useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Flex, Alert, Button, Card, Descriptions, Space, Typography } from 'antd';
import { getOne } from '@/services/api';
import { dateTime } from '@/utils/helpers';
export default function Page() {
const { id = '' } = useParams<{ id: string }>();
const [attempt, setAttempt] = useState<any>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
useEffect(() => {
let cancelled = false;
setPending(true);
setError(null);
getOne('attempts', id)
.then((result) => {
if (!cancelled) setAttempt(result);
})
.catch((loadError) => {
if (!cancelled) setError(loadError);
})
.finally(() => {
if (!cancelled) setPending(false);
});
return () => {
cancelled = true;
};
}, [id]);
if (pending) return <Card loading />;
if (error) return <Alert type="error" showIcon title={error.message} />;
if (!attempt) return null;
return (
<div>
<div style={{ marginBottom: 16 }}>
<Typography.Text type="secondary" copyable>
{attempt.id}
</Typography.Text>
</div>
<Card>
<Flex vertical gap={12}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="结果">{attempt.outcome || '执行中'}</Descriptions.Item>
<Descriptions.Item label="开始">{dateTime(attempt.started_at)}</Descriptions.Item>
<Descriptions.Item label="环境 / 出口 / 绑定版本">
{`${attempt.browser_env_alias || '未记录'} · ${attempt.network_exit_id || '未记录'} · ${attempt.binding_version || '未记录'}`}
</Descriptions.Item>
</Descriptions>
<Space wrap>
<Button type="primary" onClick={() => history.push(`/tasks/${encodeURIComponent(attempt.task_id)}`)}>
返回任务
</Button>
{attempt.browser_env_alias ? (
<Button onClick={() => history.push(`/browsers/${encodeURIComponent(attempt.browser_env_alias)}`)}>查看环境</Button>
) : null}
{attempt.network_exit_id ? (
<Button onClick={() => history.push(`/network-exits/${encodeURIComponent(attempt.network_exit_id)}`)}>查看出口</Button>
) : null}
</Space>
</Flex>
</Card>
</div>
);
}
+5 -202
View File
@@ -1,11 +1,11 @@
// 竞品分析:作品列表对齐 ant-design-pro card-list——antd Row/Col 响应式栅格 + Card 原样组合;
// 作品详情与素材处理均走 Drawer;日期筛选用 antd DatePicker;筛选自动刷新、手动翻页不变。
// 作品详情走 Drawer;日期筛选用 antd DatePicker;筛选自动刷新、手动翻页不变。
// 封面读本地缓存(/works/:id/cover,认证 fetch + blob);发布时间状态详情见 publishedAtStatusMeta 注释。
import { useEffect, useState } from 'react';
import { CommentOutlined, LikeOutlined, ShareAltOutlined } from '@ant-design/icons';
import { Alert, App, Button, Card, Col, DatePicker, Drawer, Empty, Flex, Input, InputNumber, Modal, Row, Select, Space, Spin, Table, Tag, Tooltip, Typography } from 'antd';
import { Alert, App, Button, Card, Col, DatePicker, Drawer, Empty, Flex, InputNumber, Row, Select, Space, Spin, Table, Tag, Tooltip, Typography } from 'antd';
import type { Dayjs } from 'dayjs';
import { creatorAction, creatorGet, creatorUpdate, fetchCreatorBinary, getList } from '@/services/api';
import { creatorGet, fetchCreatorBinary, getList } from '@/services/api';
import { conflictMessage, dateTime } from '@/utils/helpers';
const publishedAtStatusOptions = [
@@ -65,10 +65,6 @@ function WorkCover({ workID, title }: { workID: string; title?: string }) {
);
}
function stepTag(status: string, label: string) {
return <Tag color={status === 'succeeded' ? 'success' : status === 'failed' ? 'error' : undefined}>{label}</Tag>;
}
export default function Page() {
const [works, setWorks] = useState<any[]>([]);
const [workPage, setWorkPage] = useState(1);
@@ -79,12 +75,6 @@ export default function Page() {
const [workDetail, setWorkDetail] = useState<any>(null);
const [workMetrics, setWorkMetrics] = useState<any[]>([]);
const [workDetailPending, setWorkDetailPending] = useState(false);
const [material, setMaterial] = useState<any>(null);
const [materialPending, setMaterialPending] = useState(false);
const [rewriteRequirement, setRewriteRequirement] = useState('');
const [rewriteTitle, setRewriteTitle] = useState('');
const [rewriteScript, setRewriteScript] = useState('');
const [pendingSwitchWork, setPendingSwitchWork] = useState<string | null>(null);
const { message: messageApi } = App.useApp();
async function load() {
@@ -119,12 +109,6 @@ export default function Page() {
setWorkPage(1);
}, [filters]);
const materialDirty =
material &&
(rewriteRequirement !== (material.rewrite_requirement || '') ||
rewriteTitle !== (material.generated_title || '') ||
rewriteScript !== (material.generated_script || ''));
async function openWorkDetail(workID: string) {
setWorkDetailPending(true);
try {
@@ -141,104 +125,6 @@ export default function Page() {
}
}
function selectMaterial(workID: string) {
if (materialDirty) {
setPendingSwitchWork(workID);
return;
}
doSelectMaterial(workID);
}
async function doSelectMaterial(workID: string) {
setPendingSwitchWork(null);
setMaterialPending(true);
try {
const result = await creatorAction(`/creator/works/${encodeURIComponent(workID)}/material/select`);
setMaterial(result);
setRewriteRequirement(result.rewrite_requirement || '');
setRewriteTitle(result.generated_title || '');
setRewriteScript(result.generated_script || '');
messageApi.info('素材已选择;点击开始处理后才会执行真实下载、音轨提取和转写。');
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '素材选择失败'));
} finally {
setMaterialPending(false);
}
}
async function processMaterial() {
if (!material) return;
setMaterialPending(true);
try {
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/process`);
setMaterial(result);
const failedStep =
result.failed_step || ['download', 'audio', 'transcription'].find((step) => result[`${step}_status`] === 'failed');
if (failedStep) {
messageApi.warning(`素材处理未完成(${failedStep}):${result.failure_reason || '请查看步骤状态后重试。'}`);
} else {
messageApi.success('素材处理完成。');
}
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '素材处理失败'));
} finally {
setMaterialPending(false);
}
}
async function confirmRewrite() {
if (!material) return;
setMaterialPending(true);
try {
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite/confirm`, {
requirement: rewriteRequirement });
setMaterial(result);
messageApi.info('已确认仿写要求,请在可编辑字段中复核并保存结果。');
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '仿写确认失败'));
} finally {
setMaterialPending(false);
}
}
async function generateRewrite() {
if (!material) return;
setMaterialPending(true);
try {
const result = await creatorAction(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite/generate`);
setMaterial(result);
setRewriteTitle(result.generated_title || '');
setRewriteScript(result.generated_script || '');
messageApi.info('仿写草稿已生成,请人工复核后保存。');
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '仿写草稿生成失败'));
} finally {
setMaterialPending(false);
}
}
async function saveRewrite() {
if (!material) return;
setMaterialPending(true);
try {
const result = await creatorUpdate(`/creator/works/${encodeURIComponent(material.work_id)}/material/rewrite`, {
title: rewriteTitle,
script: rewriteScript });
setMaterial(result);
messageApi.info('仿写标题和口播已保存,可继续人工复核。');
} catch (actionError) {
messageApi.error(conflictMessage(actionError, '仿写结果保存失败'));
} finally {
setMaterialPending(false);
}
}
const materialReadyForConfirm =
material &&
material.download_status === 'succeeded' &&
!['not_started', 'running', 'failed'].includes(material.audio_status) &&
!['not_started', 'running', 'failed'].includes(material.transcription_status);
return (
<>
<Flex vertical gap={16}>
@@ -288,14 +174,6 @@ export default function Page() {
<Button key="detail" type="link" size="small" onClick={() => openWorkDetail(work.id)} disabled={workDetailPending}>
详情
</Button>,
<Button
key="select"
type={material?.work_id === work.id ? 'primary' : 'default'}
size="small"
onClick={() => selectMaterial(work.id)}
>
{material?.work_id === work.id ? '编辑素材' : '选择素材'}
</Button>,
...(work.original_url ? [<Typography.Link key="origin" href={work.original_url} target="_blank">原文</Typography.Link>] : []),
]}
>
@@ -342,73 +220,7 @@ export default function Page() {
</Flex>
) : null}
</Card>
<Drawer
open={Boolean(material)}
onClose={() => setMaterial(null)}
size="large"
title="素材处理"
destroyOnHidden
>
{material ? (
<Flex vertical gap={12}>
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>每一步都保留真实状态;失败不会被显示为成功。</Typography.Paragraph>
<Flex vertical gap={12}>
<div>
<Button type="primary" onClick={processMaterial} loading={materialPending || material.download_status === 'running'}>
开始处理素材
</Button>
</div>
<Space wrap>
{stepTag(material.download_status, `下载:${material.download_status}`)}
{stepTag(material.audio_status, `音轨:${material.audio_status}`)}
{stepTag(material.transcription_status, `转写:${material.transcription_status}`)}
</Space>
{material.failure_reason ? (
<Alert type="error" showIcon title={`失败步骤:${material.failed_step || '未记录'};${material.failure_reason}`} />
) : null}
<div>
<Typography.Text strong>仿写要求</Typography.Text>
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 4 }}>必须先确认要求,才允许保存可编辑结果。</Typography.Paragraph>
<Input.TextArea
rows={3}
value={rewriteRequirement}
onChange={(event) => setRewriteRequirement(event.target.value)}
disabled={Boolean(material.rewrite_confirmed_at) || materialPending}
/>
</div>
{material.rewrite_confirmed_at ? (
<>
<Flex gap={8} align="center">
<Button onClick={generateRewrite} disabled={materialPending}>
生成仿写草稿
</Button>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>生成结果不会自动发布,保存前必须人工复核。</Typography.Text>
</Flex>
<div>
<Typography.Text>可编辑标题</Typography.Text>
<Input value={rewriteTitle} onChange={(event) => setRewriteTitle(event.target.value)} style={{ marginTop: 4 }} />
</div>
<div>
<Typography.Text>可编辑口播</Typography.Text>
<Input.TextArea rows={5} value={rewriteScript} onChange={(event) => setRewriteScript(event.target.value)} style={{ marginTop: 4 }} />
</div>
<div>
<Button type="primary" onClick={saveRewrite} disabled={materialPending}>
保存仿写结果
</Button>
</div>
</>
) : (
<div>
<Button type="primary" onClick={confirmRewrite} disabled={materialPending || !materialReadyForConfirm}>
确认仿写要求
</Button>
</div>
)}
</Flex>
</Flex>
) : null}
</Drawer>
</Flex>
<Drawer open={Boolean(workDetail)} onClose={() => setWorkDetail(null)} size="large" title="作品详情">
{workDetail ? (
@@ -441,16 +253,7 @@ export default function Page() {
</Flex>
) : null}
</Drawer>
<Modal
open={Boolean(pendingSwitchWork)}
title="放弃未保存的素材文稿"
okText="放弃并切换"
cancelText="继续编辑"
onOk={() => pendingSwitchWork && doSelectMaterial(pendingSwitchWork)}
onCancel={() => setPendingSwitchWork(null)}
>
当前素材文稿尚未保存,确定放弃并切换吗?
</Modal>
</>
);
}
File diff suppressed because it is too large Load Diff
-212
View File
@@ -1,212 +0,0 @@
// 草稿核对:语义对齐 web.archived DraftPage.jsx(只读快照 / 版本链 / 显式核对与入队 / 关联任务)。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Alert, App, Button, Card, Checkbox, Descriptions, Flex, Modal, Space, Tag, Typography } from 'antd';
import { confirmDraft, enqueueConfirmation, getOne, getList } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
// 与账号 readiness 同构的草稿侧判定:只依赖绑定健康与调度状态。
function draftReadiness(draft: any, binding: any, bindingsError = false) {
if (draft.account.authorization_status !== 'authorized') return { label: '授权已撤销', ready: false };
if (bindingsError) return { label: '环境状态未知', ready: false };
if (!binding) return { label: '未绑定运行环境', ready: false };
if (binding.network_exit_id && binding.network_exit_health !== 'healthy') return { label: '固定出口不健康', ready: false };
const schedule = binding.schedule_block_reason || '';
return { label: schedule ? `不可调度:${schedule}` : '可调度', ready: !schedule };
}
export default function Page() {
const { id = '' } = useParams<{ id: string }>();
const [draft, setDraft] = useState<any>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [binding, setBinding] = useState<any>(null);
const [browsersError, setBrowsersError] = useState<any>(null);
const [reviewed, setReviewed] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [busy, setBusy] = useState(false);
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const result = await getOne('drafts', id);
setDraft(result);
try {
const browsersResult = await getList({ resource: 'browsers' });
setBrowsersError(null);
setBinding((browsersResult.data ?? []).find((item: any) => item.account_id === result.account_id) ?? null);
} catch (loadBindingError) {
setBrowsersError(loadBindingError);
setBinding(undefined);
}
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [id]);
useEffect(() => {
load();
}, [load]);
if (pending) return <Card loading />;
if (error || !draft) return <Alert type="error" showIcon title={error?.message || '草稿不存在'} />;
const versions = draft.versions || [];
const confirmations = draft.confirmations || [];
const tasks = draft.tasks || [];
const latestVersion = versions[0]?.version || draft.version;
const isLatest = draft.version === latestVersion;
const confirmation = confirmations.find(
(item: any) => item.account_version === draft.account.version && item.draft_version === draft.version,
);
const readiness = draftReadiness(draft, binding, Boolean(browsersError));
const canConfirm = isLatest && Boolean(binding) && !browsersError;
const canEnqueue = Boolean(confirmation) && isLatest && readiness.ready;
async function confirmSnapshot() {
setBusy(true);
try {
await confirmDraft(draft.id, draft.account.version, draft.version);
await load();
setConfirmOpen(false);
messageApi.success('确认快照已保存,可回溯账号、草稿与当前固定资源版本。');
} catch (reason) {
setConfirmOpen(false);
messageApi.error(conflictMessage(reason));
await load();
} finally {
setBusy(false);
}
}
async function enqueue() {
if (!confirmation) return;
setBusy(true);
try {
const task = await enqueueConfirmation(confirmation.id);
await load();
messageApi.success(`任务已排队:${task.id}。重复提交会返回同一任务。`);
} catch (reason) {
messageApi.error(conflictMessage(reason));
await load();
} finally {
setBusy(false);
}
}
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography.Text type="secondary" ellipsis>
{draft.account.platform_account_key} · 草稿版本 {draft.version}
</Typography.Text>
<Button onClick={() => history.push(`/accounts/${draft.account_id}`)}>返回账号详情</Button>
</div>
{browsersError ? (
<Alert type="error" showIcon style={{ marginBottom: 16 }} title="环境不可用:当前运行环境与网络出口状态未知,不能确认或排队。" />
) : null}
{!isLatest ? (
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
title={`当前只读快照已不是最新草稿版本,请刷新到版本 ${latestVersion} 后重新核对。`}
action={
<Button size="small" onClick={() => history.push(`/drafts/${versions[0]?.id}`)}>
打开最新版本
</Button>
}
/>
) : null}
<Flex vertical gap={16}>
<Card title="只读内容快照">
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{draft.content}</Typography.Paragraph>
</Card>
<Card title="版本与当前资源">
<Flex vertical gap={8}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="账号版本">{draft.account.version}</Descriptions.Item>
<Descriptions.Item label="草稿版本">{draft.version}</Descriptions.Item>
<Descriptions.Item label="运行环境">{browsersError ? '状态未知' : binding?.name || '未绑定'}</Descriptions.Item>
<Descriptions.Item label="网络出口">{browsersError ? '状态未知' : binding?.network_exit_id || '当前机器直连'}</Descriptions.Item>
</Descriptions>
<Tag color={readiness.ready ? 'success' : 'warning'}>{readiness.label}</Tag>
</Flex>
</Card>
<Card title="版本链">
<Flex vertical gap={4}>
{versions.map((item: any) =>
item.id === draft.id ? (
<Typography.Text key={item.id} strong>
版本 {item.version}(当前快照)
</Typography.Text>
) : (
<Button key={item.id} type="link" size="small" style={{ padding: 0 }} onClick={() => history.push(`/drafts/${item.id}`)}>
查看版本 {item.version}
</Button>
),
)}
</Flex>
</Card>
<Card title="显式核对与入队">
<Flex vertical gap={12}>
<Checkbox checked={reviewed} onChange={(e) => setReviewed(e.target.checked)}>
我已核对当前账号、草稿内容、运行环境和网络出口
</Checkbox>
<Space wrap>
<Button disabled={!reviewed || !canConfirm || busy} onClick={() => setConfirmOpen(true)}>
确认当前快照
</Button>
<Button type="primary" disabled={!canEnqueue || busy} loading={busy} onClick={enqueue}>
加入队列
</Button>
</Space>
{confirmation ? (
<Card size="small" title={`确认快照 v${confirmation.version}`}>
<Typography.Text>
账号版本 {confirmation.account_version} · 草稿版本 {confirmation.draft_version}
</Typography.Text>
</Card>
) : (
<Typography.Text type="secondary">保存有效确认后才可加入队列。</Typography.Text>
)}
</Flex>
</Card>
<Card title="关联任务">
{tasks.length === 0 ? (
<Typography.Text type="secondary">尚未入队。</Typography.Text>
) : (
<Flex vertical gap={8}>
{tasks.map((task: any) => (
<Card key={task.id} size="small">
<Typography.Text strong>{task.id}</Typography.Text>
<Typography.Paragraph style={{ marginBottom: 0 }}>任务状态:{task.state}</Typography.Paragraph>
</Card>
))}
</Flex>
)}
</Card>
</Flex>
<Modal
open={confirmOpen}
title="确认草稿版本"
onCancel={() => {
if (!busy) setConfirmOpen(false);
}}
onOk={confirmSnapshot}
confirmLoading={busy}
okText={busy ? '确认中…' : '保存确认'}
cancelText="返回核对"
>
<p>
将保存账号版本 {draft.account.version}、草稿版本 {draft.version}、运行环境 {binding?.alias} 与网络出口{' '}
{binding?.network_exit_id || '当前机器直连'} 的只读确认快照。版本变化后必须重新确认。
</p>
</Modal>
</div>
);
}
-250
View File
@@ -1,250 +0,0 @@
// 任务详情:语义对齐 web.archived TasksPage.jsx 的 TaskDetail。
// 四卡改用 Tabs(当前状态 / 确认快照 / 执行关联 / 唯一安全动作)——antd 默认组件的信息组织方式。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import { Alert, App, Button, Card, Descriptions, Flex, Select, Space, Tabs, Tag, Typography } from 'antd';
import { getOne, taskAction } from '@/services/api';
import { dateTime } from '@/utils/helpers';
const stateText: Record<string, string> = {
queued: '已排队',
executing: '执行中',
succeeded: '已成功',
failed: '失败',
needs_confirmation: '需要人工确认',
policy_hold: '策略暂停',
cancelled: '已取消' };
const stateColor: Record<string, string> = {
queued: 'default',
executing: 'processing',
succeeded: 'success',
failed: 'error',
needs_confirmation: 'warning',
policy_hold: 'warning',
cancelled: 'default' };
const holdText: Record<string, string> = {
account_paused: '账号已暂停',
account_revoked: '账号授权已撤销',
binding_missing: '运行环境绑定缺失',
environment_missing: '原运行环境不可用',
exit_missing: '固定出口缺失',
exit_unhealthy: '固定出口失败或发生漂移',
runtime_stop_pending: '停止结果未知,等待人工核验',
runtime_missing: '原运行实例不可用',
runtime_lease_expired: '运行实例租约已过期',
execution_lease_expired: '执行租约过期,结果未知',
task_result_uncertain: '执行结果不确定',
task_policy_hold: '执行器请求策略暂停',
account_version_changed: '账号版本已变化',
draft_version_changed: '草稿版本已变化',
confirmation_missing: '确认快照缺失',
confirmation_version_changed: '确认版本已变化',
binding_version_changed: '环境或出口版本已变化',
legacy_confirmation_required: '历史任务结果未知,等待人工核验' };
const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
function actionErrorText(error: any): string {
const reason = error?.body?.reason_code;
if (error?.status === 409) return `当前版本仍不允许恢复(409):${holdLabel(reason)}`;
if (error?.status === 503) return `原资源尚未恢复(503):${holdLabel(reason)}`;
return error?.message || '操作失败';
}
export default function Page() {
const { id = '' } = useParams<{ id: string }>();
const [task, setTask] = useState<any>(null);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const [verification, setVerification] = useState('not_executed');
const [busy, setBusy] = useState(false);
const { message: messageApi } = App.useApp();
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const result = await getOne('tasks', id);
setTask(result);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [id]);
useEffect(() => {
load();
}, [load]);
async function runAction(action: 'verify' | 'resume' | 'finish', data?: unknown) {
setBusy(true);
try {
await taskAction(task.id, action, data);
messageApi.success(
action === 'verify'
? '人工核验已记录;现在只能按该结论恢复或结束。'
: action === 'resume'
? '任务已恢复排队。'
: '任务已按核验结论结束。',
);
await load();
} catch (reason) {
messageApi.error(actionErrorText(reason));
} finally {
setBusy(false);
}
}
if (pending) return <Card loading />;
if (error || !task) return <Alert type="error" showIcon title={error?.message || '任务不存在'} />;
const confirmation = task.confirmation || {};
const unknownHold = task.hold_reason && !holdText[task.hold_reason];
const items = [
{
key: 'status',
label: '当前状态',
children: (
<Flex vertical gap={12}>
<Tag color={stateColor[task.state] || 'default'}>{stateText[task.state] || `未知状态:${task.state}`}</Tag>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="停止原因">{holdLabel(task.hold_reason)}</Descriptions.Item>
<Descriptions.Item label="人工核验">{task.verification_result || '尚未记录'}</Descriptions.Item>
</Descriptions>
</Flex>
) },
{
key: 'confirmation',
label: '确认快照',
children: (
<Flex vertical gap={12}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="确认">
<Typography.Text copyable>{confirmation.id || '未记录'}</Typography.Text> · v{confirmation.version || '—'}
</Descriptions.Item>
<Descriptions.Item label="版本">{`账号版本 ${confirmation.account_version || '—'} · 草稿版本 ${confirmation.draft_version || '—'}`}</Descriptions.Item>
<Descriptions.Item label="环境与出口">
{`${confirmation.browser_env_alias || '未记录'} · ${confirmation.network_exit_id || '未记录'} · 绑定版本:${confirmation.binding_version || '未记录'}`}
</Descriptions.Item>
</Descriptions>
<Space wrap>
<Button size="small" onClick={() => history.push(`/accounts/${task.account_id}`)}>
查看账号
</Button>
<Button size="small" onClick={() => history.push(`/drafts/${task.draft_id}`)}>
查看草稿
</Button>
</Space>
</Flex>
) },
{
key: 'execution',
label: '执行关联',
children: (
<Flex vertical gap={12}>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="环境">{task.browser_env_alias || '未记录'}</Descriptions.Item>
<Descriptions.Item label="固定出口">{task.network_exit_id || '未记录'}</Descriptions.Item>
<Descriptions.Item label="运行实例 / 绑定版本">
<Typography.Text copyable>{task.runtime_instance_id || '未记录'}</Typography.Text> · {task.binding_version || '未记录'}
</Descriptions.Item>
</Descriptions>
<Space wrap>
{task.browser_env_alias ? (
<Button size="small" onClick={() => history.push(`/browsers/${encodeURIComponent(task.browser_env_alias)}`)}>
查看原环境
</Button>
) : null}
{task.network_exit_id ? (
<Button size="small" onClick={() => history.push(`/network-exits/${encodeURIComponent(task.network_exit_id)}`)}>
查看原出口
</Button>
) : null}
</Space>
</Flex>
) },
{
key: 'action',
label: '唯一安全动作',
children: (
<Flex vertical gap={12}>
{task.readiness_reason ? <Alert type="info" showIcon title={`当前仍被阻断:${holdLabel(task.readiness_reason)}。修复原资源并刷新后,才会开放恢复。`} /> : null}
{task.allowed_action === 'verify' ? (
<>
<Alert type="info" showIcon title='先在原环境和原出口核验真实结果。选择"未执行"不会立即重试,仍需下一步显式恢复。' />
<Select
style={{ width: 280 }}
value={verification}
onChange={setVerification}
options={[
{ value: 'not_executed', label: '确认未执行,可评估恢复' },
{ value: 'succeeded', label: '确认已成功' },
{ value: 'failed', label: '确认已失败' },
]}
/>
<div>
<Button type="primary" disabled={busy} onClick={() => runAction('verify', { result: verification })}>
记录人工核验
</Button>
</div>
</>
) : null}
{task.allowed_action === 'resume' ? (
<Button type="primary" disabled={busy} onClick={() => runAction('resume')}>
按核验结论恢复排队
</Button>
) : null}
{task.allowed_action === 'finish' ? (
<Button type="primary" disabled={busy} onClick={() => runAction('finish')}>
按核验结论结束任务
</Button>
) : null}
{task.allowed_action === 'reconfirm' ? (
<>
<Alert type="info" showIcon title="版本已变化,旧确认不可继续使用。" />
<Button type="primary" onClick={() => history.push(`/drafts/${task.draft_id}`)}>
重新核对并确认草稿
</Button>
</>
) : null}
{task.allowed_action ? null : <Typography.Text type="secondary">当前没有可安全执行的动作。</Typography.Text>}
</Flex>
) },
];
return (
<Card variant="outlined">
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ minWidth: 0 }}>
<Typography.Title level={4} style={{ marginBottom: 0 }}>任务详情</Typography.Title>
<Typography.Text type="secondary" copyable>
{task.id}
</Typography.Text>
</div>
<Button onClick={() => history.push('/tasks')}>返回任务中心</Button>
</div>
{unknownHold ? <Alert type="warning" showIcon style={{ marginBottom: 16 }} title={`后端返回了未知停止原因"${task.hold_reason}";为避免不安全操作,本页不提供恢复动作。`} /> : null}
<Tabs defaultActiveKey="status" items={items} />
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>执行尝试</Typography.Title>
{task.attempts?.length ? (
<Flex vertical gap={8}>
{task.attempts.map((attempt: any) => (
<Card key={attempt.id} size="small">
<Typography.Link onClick={() => history.push(`/attempts/${encodeURIComponent(attempt.id)}`)}>{attempt.id}</Typography.Link>
<Typography.Paragraph style={{ marginTop: 6, marginBottom: 0 }}>
{attempt.outcome || '执行中'} · {dateTime(attempt.started_at)}
</Typography.Paragraph>
<Typography.Text type="secondary">脱敏证据:{attempt.evidence?.mock_outcome || '无可公开证据'}</Typography.Text>
</Card>
))}
</Flex>
) : (
<Card size="small">
<Typography.Text type="secondary">尚未产生执行尝试。</Typography.Text>
</Card>
)}
</Card>
);
}
-185
View File
@@ -1,185 +0,0 @@
// 任务中心列表:语义对齐 web.archived TasksPage.jsx 的 TaskList。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, Button, Card, Flex, Select, Space, Table, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getList } from '@/services/api';
import { dateTime } from '@/utils/helpers';
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
const stateText: Record<string, string> = {
queued: '已排队',
executing: '执行中',
succeeded: '已成功',
failed: '失败',
needs_confirmation: '需要人工确认',
policy_hold: '策略暂停',
cancelled: '已取消',
};
const stateColor: Record<string, string> = {
queued: 'default',
executing: 'processing',
succeeded: 'success',
failed: 'error',
needs_confirmation: 'warning',
policy_hold: 'warning',
cancelled: 'default',
};
const holdText: Record<string, string> = {
account_paused: '账号已暂停',
account_revoked: '账号授权已撤销',
binding_missing: '运行环境绑定缺失',
environment_missing: '原运行环境不可用',
exit_missing: '固定出口缺失',
exit_unhealthy: '固定出口失败或发生漂移',
runtime_stop_pending: '停止结果未知,等待人工核验',
runtime_missing: '原运行实例不可用',
runtime_lease_expired: '运行实例租约已过期',
execution_lease_expired: '执行租约过期,结果未知',
task_result_uncertain: '执行结果不确定',
task_policy_hold: '执行器请求策略暂停',
account_version_changed: '账号版本已变化',
draft_version_changed: '草稿版本已变化',
confirmation_missing: '确认快照缺失',
confirmation_version_changed: '确认版本已变化',
binding_version_changed: '环境或出口版本已变化',
legacy_confirmation_required: '历史任务结果未知,等待人工核验',
};
export const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
export default function Page() {
const [accountFilter, setAccountFilter] = useState<string | undefined>(undefined);
const [stateFilter, setStateFilter] = useState<string | undefined>(undefined);
const [rows, setRows] = useState<any[]>([]);
const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]);
const [accountsPending, setAccountsPending] = useState(false);
const [pending, setPending] = useState(true);
const [error, setError] = useState<any>(null);
const { vertical, horizontal } = useOverflowGrid(rows.length);
const load = useCallback(async () => {
setPending(true);
setError(null);
try {
const result = await getList({
resource: 'tasks',
filters: { account_id: accountFilter, state: stateFilter || undefined },
});
setRows(result.data ?? []);
} catch (loadError) {
setError(loadError);
} finally {
setPending(false);
}
}, [accountFilter, stateFilter]);
useEffect(() => {
load();
}, [load]);
// 账号筛选下拉:一次性拉取账号列表,无需手输 UUID。
useEffect(() => {
setAccountsPending(true);
getList({ resource: 'accounts' })
.then((result) =>
setAccountOptions(
(result.data ?? []).map((account: any) => ({
value: account.id,
label: account.name ? `${account.name}(${account.id})` : account.id,
})),
),
)
.catch(() => setAccountOptions([]))
.finally(() => setAccountsPending(false));
}, []);
const columns: ColumnsType<any> = [
{
title: '任务',
dataIndex: 'id',
...fixedLeft<any>({}),
render: (value: string) => (
<Space size={4}>
<Typography.Link onClick={() => history.push(`/tasks/${value}`)}>{value}</Typography.Link>
<Typography.Text copyable={{ text: value }} style={{ fontSize: 12 }} />
</Space>
),
},
{
title: '账号',
dataIndex: 'account_id',
render: (value: string) => (
<Space size={4}>
<Typography.Link onClick={() => history.push(`/accounts/${value}`)}>{value}</Typography.Link>
<Typography.Text copyable={{ text: value }} style={{ fontSize: 12 }} />
</Space>
),
},
{ title: '确认', dataIndex: 'confirmation_id', render: (value?: string) => value || '未确认' },
{
title: '状态',
dataIndex: 'state',
render: (value: string) => <Tag color={stateColor[value] || 'default'}>{stateText[value] || `未知状态:${value}`}</Tag>,
},
{ title: '停止原因', dataIndex: 'hold_reason', render: (value?: string) => <Typography.Text type="secondary">{holdLabel(value)}</Typography.Text> },
{
title: '更新时间',
dataIndex: 'updated_at',
render: (_, task) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{dateTime(task.updated_at || task.created_at)}</Typography.Text>,
},
{
title: '操作',
key: 'actions',
align: 'right',
...fixedRight<any>({}),
render: (_, task) => (
<Button type="text" size="small" onClick={() => history.push(`/tasks/${task.id}`)}>
查看
</Button>
),
},
];
return (
<div>
<Card variant="outlined" style={{ marginBottom: 16 }}>
<Flex justify="space-between" align="center">
<Typography.Text type="secondary">共 {rows.length} 个任务</Typography.Text>
<Space wrap>
<Select
style={{ width: 280 }}
placeholder="全部账号"
allowClear
showSearch={{ optionFilterProp: 'label' }}
loading={accountsPending && accountOptions.length === 0}
value={accountFilter}
onChange={setAccountFilter}
options={accountOptions}
/>
<Select
style={{ width: 200 }}
placeholder="全部状态"
allowClear
value={stateFilter}
onChange={setStateFilter}
options={Object.entries(stateText).map(([value, label]) => ({ value, label }))}
/>
</Space>
</Flex>
</Card>
<Card variant="outlined">
{error ? <Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} /> : null}
<Table
rowKey="id"
columns={columns}
dataSource={rows}
loading={pending}
sticky
pagination={tablePagination()}
scroll={tableScroll(horizontal ? 960 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }} />
</Card>
</div>
);
}
+1 -33
View File
@@ -6,10 +6,6 @@ export const resourcePaths = {
browsers: '/browsers',
gateways: '/gateways',
accounts: '/phase-a/accounts',
drafts: '/phase-a/drafts',
confirmations: '/phase-a/confirmations',
tasks: '/phase-a/tasks',
attempts: '/phase-a/attempts',
'network-exits': '/network-exits',
'creator-accounts': '/creator/accounts',
'creator-competitors': '/creator/competitors',
@@ -18,27 +14,17 @@ export const resourcePaths = {
'creator-comments': '/creator/comments',
'creator-leads': '/creator/leads',
'creator-rules': '/creator/rules',
'creator-operations': '/creator/operations',
'creator-conversations': '/creator/conversations',
'creator-events': '/creator/events',
'creator-listeners': '/creator/listeners',
} as const;
export type Resource = keyof typeof resourcePaths;
// 与归档版 filterKeys 一致:资源 → 允许透传的查询字段。
const filterKeys: Partial<Record<Resource, string[]>> = {
drafts: ['account_id'],
confirmations: ['draft_id'],
tasks: ['account_id', 'draft_id', 'state'],
'creator-competitors': ['platform'],
'creator-competitor-share-jobs': ['platform', 'status'],
'creator-works': ['platform', 'source_id', 'source_type', 'published_at_status', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares', 'growth_hours', 'min_likes_growth'],
'creator-comments': ['platform', 'work_id'],
'creator-leads': ['platform'],
'creator-operations': ['account_id'],
'creator-conversations': ['account_id'],
'creator-events': ['account_id'],
};
export interface ListParams {
@@ -70,7 +56,7 @@ export async function getList<T = any>({ resource, page, pageSize, filters = {}
};
}
const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'drafts', 'confirmations', 'tasks', 'attempts', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules', 'creator-operations'];
const GETONE_ALLOWED: Resource[] = ['accounts', 'network-exits', 'browsers', 'creator-accounts', 'creator-competitors', 'creator-competitor-share-jobs', 'creator-works', 'creator-comments', 'creator-rules'];
export async function getOne<T = any>(resource: Resource, id: string | number): Promise<T> {
if (!GETONE_ALLOWED.includes(resource)) throw new Error(`${resource} 不支持 getOne`);
@@ -109,24 +95,6 @@ export async function accountAction(id: string, action: 'pause' | 'resume'): Pro
await request(`/phase-a/accounts/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
}
export function createDraft(accountID: string, content: string) {
return request('/phase-a/drafts', jsonOptions('POST', { account_id: accountID, content }));
}
export function confirmDraft(draftID: string, accountVersion: number, draftVersion: number) {
return request('/phase-a/confirmations', jsonOptions('POST', { draft_id: draftID, account_version: accountVersion, draft_version: draftVersion }));
}
export function enqueueConfirmation(confirmationID: string) {
return request('/phase-a/tasks', jsonOptions('POST', { confirmation_id: confirmationID }));
}
export async function taskAction(id: string, action: 'verify' | 'resume' | 'finish' | 'cancel', data?: unknown): Promise<void> {
if (!['verify', 'resume', 'finish', 'cancel'].includes(action)) throw new Error(`未知任务操作: ${action}`);
const options = data ? jsonOptions('POST', data) : { method: 'POST' };
await request(`/phase-a/tasks/${encodeURIComponent(id)}/${action}`, options);
}
export async function networkExitAction(id: string, action: 'check' | 'disable' | 'enable'): Promise<any> {
if (!['check', 'disable', 'enable'].includes(action)) throw new Error(`未知网络出口操作: ${action}`);
return request(`/network-exits/${encodeURIComponent(id)}/${action}`, { method: 'POST' });
-6
View File
@@ -1,17 +1,11 @@
// 列表页共享工具:资源 → 查询字段白名单,对齐 web.archived/src/shared/api/dataProvider.js 的 filterKeys。
// creator-* 资源走 page/page_size 分页;其余资源后端返回全量或自带 total。
export const filterKeys: Record<string, string[]> = {
drafts: ['account_id'],
confirmations: ['draft_id'],
tasks: ['account_id', 'draft_id', 'state'],
'creator-competitors': ['platform'],
'creator-competitor-share-jobs': ['platform', 'status'],
'creator-works': ['platform', 'source_id', 'source_type', 'published_after', 'published_before', 'min_likes', 'min_comments', 'min_shares'],
'creator-comments': ['platform', 'work_id'],
'creator-leads': ['platform'],
'creator-operations': ['account_id'],
'creator-conversations': ['account_id'],
'creator-events': ['account_id'],
};
export function isCreatorResource(resource: string): boolean {
-7
View File
@@ -24,13 +24,8 @@ const rules: Array<{ test: (path: string) => boolean; title: string; subtitle: s
{ test: (p) => /\/edit$/.test(p) && p.startsWith('/accounts/'), title: '编辑社媒账号', subtitle: '维护账号资料、登录核验与自动响应策略。' },
{ test: (p) => p.startsWith('/accounts/'), title: '账号详情', subtitle: '查看账号状态、登录身份与运行环境绑定。' },
{ test: (p) => p === '/creator/competitors', title: '竞品分析', subtitle: '查看竞品作品与指标,分享链接导入请前往账号导入。' },
{ test: (p) => p === '/creator/workbench', title: '运营工作台', subtitle: '评论、线索、私信与写操作均保留来源和明确结果。' },
{ test: (p) => p === '/creator/settings', title: '采集设置', subtitle: '统一配置采集窗口、指标采集和已批准的服务。' },
{ test: (p) => p.startsWith('/creator/competitors/'), title: '竞品账号详情', subtitle: '查看竞品画像与作品列表,点击作品可追溯指标统计。' },
{ test: (p) => p.startsWith('/drafts/'), title: '草稿核对', subtitle: '核对版本化草稿,确认后再将操作加入队列。' },
{ test: (p) => p.startsWith('/attempts/'), title: 'Attempt 详情', subtitle: '查看执行结果、运行环境与网络出口。' },
{ test: (p) => p === '/tasks', title: '任务中心', subtitle: '逐条处理暂停、版本变化与未知执行结果。' },
{ test: (p) => p.startsWith('/tasks/'), title: '任务详情', subtitle: '查看任务状态、执行结果与允许的下一步操作。' },
{ test: (p) => p === '/browsers', title: '运行环境', subtitle: '启动、停止并回收隔离的指纹浏览器运行环境。' },
{ test: (p) => p === '/browsers/new', title: '创建运行环境', subtitle: '为已授权且暂停的账号绑定网关、指纹与网络出口。' },
{ test: (p) => p.startsWith('/browsers/'), title: '运行环境详情', subtitle: '查看环境状态、账号绑定和运行实例信息。' },
@@ -50,9 +45,7 @@ export const menu: MenuDataItem[] = [
{ path: '/accounts', name: '我的账号', icon: <UserOutlined /> },
{ path: '/accounts/monitoring', name: '监控账号', icon: <EyeOutlined /> },
{ path: '/creator/competitors', name: '竞品分析', icon: <LineChartOutlined /> },
{ path: '/creator/workbench', name: '运营工作台', icon: <CommentOutlined /> },
{ path: '/creator/settings', name: '采集设置', icon: <SettingOutlined /> },
{ path: '/tasks', name: '任务中心', icon: <ProfileOutlined /> },
],
},
{