feat(web): task-4 tasks 模块迁移(任务中心列表/详情/attempts/drafts,Tabs 信息组织,动作与筛选语义对齐归档版)
This commit is contained in:
@@ -1,4 +1,71 @@
|
||||
// TODO: 从 web.archived 迁移 attempts/$id
|
||||
// Attempt 详情:语义对齐 web.archived TracePages.jsx 的 AttemptDetail。
|
||||
import { useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Alert, Button, Card, Descriptions, Space, Typography } from 'antd';
|
||||
import { getOne } from '@/services/api';
|
||||
import { dateTime } from '@/utils/helpers';
|
||||
|
||||
export default function Page() {
|
||||
return null;
|
||||
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 message={error.message} />;
|
||||
if (!attempt) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: 0 }}>
|
||||
Attempt 详情
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary" copyable>
|
||||
{attempt.id}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Card>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<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>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,227 @@
|
||||
// TODO: 从 web.archived 迁移 drafts/$id
|
||||
// 草稿核对:语义对齐 web.archived DraftPage.jsx(只读快照 / 版本链 / 显式核对与入队 / 关联任务)。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Descriptions,
|
||||
Modal,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} 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() {
|
||||
return null;
|
||||
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 [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
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 message={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>
|
||||
{contextHolder}
|
||||
<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" ellipsis>
|
||||
{draft.account.platform_account_key} · 草稿版本 {draft.version}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button onClick={() => history.push(`/accounts/${draft.account_id}`)}>返回账号详情</Button>
|
||||
</div>
|
||||
{browsersError ? (
|
||||
<Alert type="error" showIcon style={{ marginBottom: 16 }} message="环境不可用:当前运行环境与网络出口状态未知,不能确认或排队。" />
|
||||
) : null}
|
||||
{!isLatest ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message={`当前只读快照已不是最新草稿版本,请刷新到版本 ${latestVersion} 后重新核对。`}
|
||||
action={
|
||||
<Button size="small" onClick={() => history.push(`/drafts/${versions[0]?.id}`)}>
|
||||
打开最新版本
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<Card title="只读内容快照">
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{draft.content}</Typography.Paragraph>
|
||||
</Card>
|
||||
<Card title="版本与当前资源">
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<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>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card title="版本链">
|
||||
<Space direction="vertical" size={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>
|
||||
),
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
<Card title="显式核对与入队">
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<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>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
<Card title="关联任务">
|
||||
{tasks.length === 0 ? (
|
||||
<Typography.Text type="secondary">尚未入队。</Typography.Text>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={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>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
</Space>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,258 @@
|
||||
// TODO: 从 web.archived 迁移 tasks/$id
|
||||
// 任务详情:语义对齐 web.archived TasksPage.jsx 的 TaskDetail。
|
||||
// 四卡改用 Tabs(当前状态 / 确认快照 / 执行关联 / 唯一安全动作)——antd 默认组件的信息组织方式。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Alert, Button, Card, Descriptions, Select, Space, Tabs, Tag, Typography, message } 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() {
|
||||
return null;
|
||||
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 [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
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 message={error?.message || '任务不存在'} />;
|
||||
|
||||
const confirmation = task.confirmation || {};
|
||||
const unknownHold = task.hold_reason && !holdText[task.hold_reason];
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '当前状态',
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<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>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'confirmation',
|
||||
label: '确认快照',
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<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>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'execution',
|
||||
label: '执行关联',
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
<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>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
label: '唯一安全动作',
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={16}>
|
||||
{task.readiness_reason ? <Alert type="info" showIcon message={`当前仍被阻断:${holdLabel(task.readiness_reason)}。修复原资源并刷新后,才会开放恢复。`} /> : null}
|
||||
{task.allowed_action === 'verify' ? (
|
||||
<>
|
||||
<Alert type="info" showIcon message='先在原环境和原出口核验真实结果。选择"未执行"不会立即重试,仍需下一步显式恢复。' />
|
||||
<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 message="版本已变化,旧确认不可继续使用。" />
|
||||
<Button type="primary" onClick={() => history.push(`/drafts/${task.draft_id}`)}>
|
||||
重新核对并确认草稿
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{task.allowed_action ? null : <Typography.Text type="secondary">当前没有可安全执行的动作。</Typography.Text>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<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 }} message={`后端返回了未知停止原因"${task.hold_reason}";为避免不安全操作,本页不提供恢复动作。`} /> : null}
|
||||
<Tabs defaultActiveKey="status" items={items} />
|
||||
<Typography.Title level={5} style={{ marginTop: 24, marginBottom: 12 }}>执行尝试</Typography.Title>
|
||||
{task.attempts?.length ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
{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>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Card size="small">
|
||||
<Typography.Text type="secondary">尚未产生执行尝试。</Typography.Text>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,143 @@
|
||||
// TODO: 从 web.archived 迁移 tasks/index
|
||||
// 任务中心列表:语义对齐 web.archived TasksPage.jsx 的 TaskList。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history } from '@umijs/max';
|
||||
import { Alert, Button, Input, Select, Space, Table, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getList } 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: '历史任务结果未知,等待人工核验',
|
||||
};
|
||||
export const holdLabel = (reason?: string) => (reason ? holdText[reason] || `未知停止原因:${reason}` : '无');
|
||||
|
||||
export default function Page() {
|
||||
return null;
|
||||
const [accountFilter, setAccountFilter] = useState('');
|
||||
const [stateFilter, setStateFilter] = useState<string | undefined>(undefined);
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [pending, setPending] = useState(true);
|
||||
const [error, setError] = useState<any>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await getList({
|
||||
resource: 'tasks',
|
||||
filters: { account_id: accountFilter || undefined, state: stateFilter || undefined },
|
||||
});
|
||||
setRows(result.data ?? []);
|
||||
} catch (loadError) {
|
||||
setError(loadError);
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [accountFilter, stateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{
|
||||
title: '任务',
|
||||
dataIndex: 'id',
|
||||
render: (value: string) => <Typography.Link onClick={() => history.push(`/tasks/${value}`)}>{value}</Typography.Link>,
|
||||
},
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'account_id',
|
||||
render: (value: string) => <Typography.Link onClick={() => history.push(`/accounts/${value}`)}>{value}</Typography.Link>,
|
||||
},
|
||||
{ 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',
|
||||
render: (_, task) => (
|
||||
<Button size="small" onClick={() => history.push(`/tasks/${task.id}`)}>
|
||||
查看
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Paragraph type="secondary">逐条处理暂停、版本变化与未知执行结果;不会自动重试或更换出口</Typography.Paragraph>
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
style={{ width: 280 }}
|
||||
placeholder="账号 ID"
|
||||
value={accountFilter}
|
||||
onChange={(event) => setAccountFilter(event.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 200 }}
|
||||
placeholder="全部状态"
|
||||
allowClear
|
||||
value={stateFilter}
|
||||
onChange={setStateFilter}
|
||||
options={Object.entries(stateText).map(([value, label]) => ({ value, label }))}
|
||||
/>
|
||||
</Space>
|
||||
{error ? <Alert type="error" showIcon style={{ marginBottom: 16 }} message={error.message} action={<Button size="small" onClick={load}>重试</Button>} /> : null}
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={pending}
|
||||
pagination={false}
|
||||
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user