fix(automation): 修复作业完成状态回写崩溃、统计字段对齐及表单布局挤压
1. 修复 DouyuBatchRunner 在 run 结束时私自关闭外部 Session 导致作业状态无法回写并死锁在 running 的问题(增加 close_db 参数); 2. 修复资料补全作业返回值映射,将 succeeded/failed 对齐到 valid/invalid,并在全库暂无待查账号时给出清晰说明; 3. 增强调度器 _run_one 边界防御:支持字段名兜底与 Session 崩溃时独立连接兜底回写,防止作业无限重复发起; 4. 修复巡检作业在全库无到期账号时的说明信息,如实展示最久检测时间与重检间隔; 5. 修复 AutomationPage 布局:移除外层 Form 的 layout=inline,Card 恢复 100% 满宽纵向排列,内部字段横向弹性自适应; 6. 修复未启用作业因残留历史 next_run_at 而错误显示「排队中」的问题。
This commit is contained in:
@@ -38,15 +38,13 @@ const STATUS_TAG: Record<string, { color: string; text: string }> = {
|
||||
skipped: { color: "default", text: "已跳过" },
|
||||
};
|
||||
|
||||
/**
|
||||
* 下次运行时间:两个作业串行跑在同一个调度线程里,前一个作业耗时长时,
|
||||
* 后一个会排队等待 —— 于是 next_run_at 会停在「已经过了」的时刻。
|
||||
* 直接显示一个过去时间看起来像故障,这里标明「排队中」。
|
||||
*/
|
||||
function nextRunLabel(job: AutomationJobItem): ReactNode {
|
||||
if (!job.next_run_at) {
|
||||
if (!job.enabled) {
|
||||
return <Typography.Text type="secondary">未启用</Typography.Text>;
|
||||
}
|
||||
if (!job.next_run_at) {
|
||||
return <Typography.Text type="secondary">-</Typography.Text>;
|
||||
}
|
||||
const next = new Date(job.next_run_at).getTime();
|
||||
if (!Number.isFinite(next)) return formatTime(job.next_run_at);
|
||||
if (next > Date.now()) return formatTime(job.next_run_at);
|
||||
@@ -54,7 +52,7 @@ function nextRunLabel(job: AutomationJobItem): ReactNode {
|
||||
<span>
|
||||
<Typography.Text type="warning">排队中</Typography.Text>{" "}
|
||||
<Typography.Text type="secondary">
|
||||
({formatTime(job.next_run_at)} 到期,等待前序作业让出线程)
|
||||
({formatTime(job.next_run_at)} 到期)
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
@@ -273,116 +271,129 @@ export default function AutomationPage() {
|
||||
description="两个作业是联动的上下游:巡检把失效账号标记为 invalid,「CK 自动重登」的候选条件正是这个标记,所以巡检一发现失效,重登作业在下一个周期就会把它救回来(需打开重登开关)。刻意分成两个作业是为了止损粒度 —— 巡检只读、零风险;重登会真实登录(消耗邮箱验证码、可能遇到极验),出问题可单独关掉重登而不影响检测。巡检本身不会替换任何 Cookie、也不会改账号状态。"
|
||||
/>
|
||||
|
||||
<Form form={forms} layout="inline">
|
||||
{/* ⚠️ 必须有个纵向容器:`layout="inline"` 会把直接子元素排成一行,
|
||||
5 张 Card 会横着挤在一起且丢掉间距(真踩过)。 */}
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{jobs.map((job) => (
|
||||
<Card
|
||||
key={job.key}
|
||||
type="inner"
|
||||
title={job.title}
|
||||
extra={
|
||||
job.key === "ck_relogin" ? (
|
||||
<Typography.Text type="secondary">
|
||||
上游:巡检标记为失效且有 CK 的账号,当前待重登{" "}
|
||||
<Typography.Text strong>{pendingRelogin}</Typography.Text> 个
|
||||
</Typography.Text>
|
||||
) : job.key === "ck_health" ? (
|
||||
<Typography.Text type="secondary">
|
||||
下游:CK 自动重登(发现失效后自动接手)
|
||||
</Typography.Text>
|
||||
) : null
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Descriptions size="small" column={2} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="最近运行">
|
||||
{formatTime(job.last_run_at)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="下次运行">
|
||||
{nextRunLabel(job)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近结果">
|
||||
{job.last_status ? (
|
||||
<Tag color={STATUS_TAG[job.last_status]?.color ?? "default"}>
|
||||
{STATUS_TAG[job.last_status]?.text ?? job.last_status}
|
||||
</Tag>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近说明">
|
||||
{job.last_message || "-"}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Form form={forms} layout="vertical">
|
||||
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
||||
{jobs.map((job) => (
|
||||
<Card
|
||||
key={job.key}
|
||||
type="inner"
|
||||
title={job.title}
|
||||
extra={
|
||||
job.key === "ck_relogin" ? (
|
||||
<Typography.Text type="secondary">
|
||||
上游:巡检标记为失效且有 CK 的账号,当前待重登{" "}
|
||||
<Typography.Text strong>{pendingRelogin}</Typography.Text> 个
|
||||
</Typography.Text>
|
||||
) : job.key === "ck_health" ? (
|
||||
<Typography.Text type="secondary">
|
||||
下游:CK 自动重登(发现失效后自动接手)
|
||||
</Typography.Text>
|
||||
) : null
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Descriptions size="small" column={2} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="最近运行">
|
||||
{formatTime(job.last_run_at)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="下次运行">
|
||||
{nextRunLabel(job)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近结果">
|
||||
{job.last_status ? (
|
||||
<Tag color={STATUS_TAG[job.last_status]?.color ?? "default"}>
|
||||
{STATUS_TAG[job.last_status]?.text ?? job.last_status}
|
||||
</Tag>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近说明">
|
||||
{job.last_message || "-"}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* ⚠️ 这里**不能**各自包一个 <Form form={forms}>:
|
||||
多个 <Form> 绑同一个 form 实例时,`onFinish` 会被最后挂载的那个覆盖 ——
|
||||
点第一张卡的保存,跑的其实是**最后一张卡**的保存(真踩过:
|
||||
存「账号资料定时补全」提示的是「积分到期补记」,即存错了作业)。
|
||||
Form 统一提到这一层外面,各卡只用 Form.Item 的 [job.key, 字段] 区分。 */}
|
||||
{/* 字段行:显式 gap 撑开,别让输入框挤在一起 */}
|
||||
<Space wrap size={16} align="end" style={{ rowGap: 8 }}>
|
||||
<Form.Item
|
||||
name={[job.key, "enabled"]}
|
||||
label="启用"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={[job.key, "run_interval_minutes"]}
|
||||
label="多久跑一次(分钟)"
|
||||
tooltip="调度频率:上一轮跑完后隔多久开始下一轮。这是作业「多久跑一次」。"
|
||||
>
|
||||
<InputNumber min={0.5} step={5} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={[job.key, "interval_hours"]}
|
||||
label="同一账号多久重检(小时)"
|
||||
tooltip="同一个账号多久重新检测/重试一次。它**不决定**作业多久跑一次 —— 那是左边那一项。"
|
||||
>
|
||||
<InputNumber min={0.1} step={1} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[job.key, "batch_size"]} label="每轮条数">
|
||||
<InputNumber min={1} max={2000} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[job.key, "concurrency"]} label="并发">
|
||||
<InputNumber min={1} max={16} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space size={8} style={{ marginLeft: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canManage}
|
||||
loading={savingKey === job.key}
|
||||
onClick={() => void saveJob(job)}
|
||||
{/* 字段行:使用 flex 容器水平对齐,底端对齐,自动折行 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "flex-end",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name={[job.key, "enabled"]}
|
||||
label="启用"
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="立刻跑一轮?"
|
||||
description="不看启用开关,立即执行一次巡检。"
|
||||
onConfirm={() => runOnce(job)}
|
||||
disabled={!canManage}
|
||||
<Switch disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={[job.key, "run_interval_minutes"]}
|
||||
label="多久跑一次(分钟)"
|
||||
tooltip="调度频率:上一轮跑完后隔多久开始下一轮。这是作业「多久跑一次」。"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber min={0.5} step={5} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={[job.key, "interval_hours"]}
|
||||
label="同一账号多久重检(小时)"
|
||||
tooltip="同一个账号多久重新检测/重试一次。它**不决定**作业多久跑一次 —— 那是左边那一项。"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber min={0.1} step={1} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={[job.key, "batch_size"]}
|
||||
label="每轮条数"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber min={1} max={2000} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={[job.key, "concurrency"]}
|
||||
label="并发"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<InputNumber min={1} max={16} disabled={!canManage} />
|
||||
</Form.Item>
|
||||
<Space size={8} style={{ marginBottom: 0, paddingBottom: 0 }}>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
type="primary"
|
||||
disabled={!canManage}
|
||||
loading={runningKey === job.key}
|
||||
loading={savingKey === job.key}
|
||||
onClick={() => void saveJob(job)}
|
||||
>
|
||||
立即跑一次
|
||||
保存
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
{!canManage && (
|
||||
<Typography.Text type="secondary">
|
||||
当前账号只能查看,修改配置需要 automation:manage 权限。
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
<Popconfirm
|
||||
title="立刻跑一轮?"
|
||||
description="不看启用开关,立即执行一次巡检。"
|
||||
onConfirm={() => runOnce(job)}
|
||||
disabled={!canManage}
|
||||
>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
disabled={!canManage}
|
||||
loading={runningKey === job.key}
|
||||
>
|
||||
立即跑一次
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
{!canManage && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Typography.Text type="secondary">
|
||||
当前账号只能查看,修改配置需要 automation:manage 权限。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user