Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions src/components/DashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import { getInterfaceLangKey } from '@/i18n';
import { getMxuSpecialTask } from '@/types/specialTasks';
import { isTaskCompatible } from '@/stores/helpers';
import { isPretaskName, getPretaskItem, buildPretaskArgs, buildPretaskDef } from '@/types/pretasks';
import { getWeekdayOf, filterTasksForRun, createTaskRunSelection } from '@/utils/taskRunFilter';
import { resolveTaskLabel } from '@/utils/taskLabel';
import { useCurrentWeekday } from '@/hooks/useCurrentWeekday';
import { startGlobalCallbackListener } from '@/components/connection/callbackCache';
import { stopInstanceTasks } from '@/services/taskStopService';
import { buildPiEnvVars } from '@/utils/piEnv';
Expand Down Expand Up @@ -76,6 +79,7 @@ function InstanceCard({ instanceId, instanceName, isActive, onSelect }: Instance
// 任务控制相关
updateInstance,
setInstanceTaskStatus,
setTaskRunSelection,
setInstanceCurrentTaskId,
clearScheduleExecution,
basePath,
Expand Down Expand Up @@ -116,8 +120,12 @@ function InstanceCard({ instanceId, instanceName, isActive, onSelect }: Instance
const instance = instances.find((i) => i.id === instanceId);
const isRunning = instance?.isRunning || false;
const tasks = instance?.selectedTasks || [];
const enabledTasks = tasks.filter((t) => t.enabled);
const canRun = isConnected && isResourceLoaded && enabledTasks.length > 0;
// 周计划:只有已勾选、且今天未被周计划跳过的任务参与运行。
// 本视图的启动流程是独立实现(不走 Toolbar.startTasksForInstance),
// 因此这里必须自行过滤,否则周计划在 Dashboard 视图下会失效。
const todayWeekday = useCurrentWeekday();
const runPreview = filterTasksForRun(tasks, todayWeekday);
const canRun = isConnected && isResourceLoaded && runPreview.tasks.length > 0;

// 获取当前控制器和资源名(用于 pipeline override 生成)
const currentControllerName =
Expand Down Expand Up @@ -212,7 +220,7 @@ function InstanceCard({ instanceId, instanceName, isActive, onSelect }: Instance
async (e: React.MouseEvent) => {
e.stopPropagation();

if (!instance || (!canRun && !isRunning)) return;
if (!instance) return;

if (isRunning) {
// 停止任务
Expand All @@ -230,7 +238,28 @@ function InstanceCard({ instanceId, instanceName, isActive, onSelect }: Instance
}
} else {
// 启动任务
if (!canRun) return;
if (!isConnected || !isResourceLoaded) return;

// 以点击启动时刻和最新任务列表构建快照,运行中跨天不再改变本批任务。
const runWeekday = getWeekdayOf();
const runTasks =
useAppStore.getState().instances.find((item) => item.id === instanceId)?.selectedTasks ||
[];
const runSelection = filterTasksForRun(runTasks, runWeekday);
const enabledTasks = runSelection.tasks;
setTaskRunSelection(instanceId, createTaskRunSelection(runSelection, runWeekday));

// 周计划:记录本次被跳过的任务,避免"任务莫名没跑"
runSelection.skippedByWeeklySchedule.forEach((task) => {
addLog(instanceId, {
type: 'info',
message: t('taskList.taskSkippedByWeeklySchedule', {
taskName: resolveTaskLabel(task, projectInterface, translations, t),
}),
});
});

if (enabledTasks.length === 0) return;

setIsStarting(true);

Expand Down Expand Up @@ -433,12 +462,13 @@ function InstanceCard({ instanceId, instanceName, isActive, onSelect }: Instance
instanceId,
instanceName,
isRunning,
canRun,
enabledTasks,
isConnected,
isResourceLoaded,
projectInterface,
basePath,
updateInstance,
setInstanceTaskStatus,
setTaskRunSelection,
setInstanceCurrentTaskId,
clearScheduleExecution,
registerTaskIdName,
Expand Down Expand Up @@ -781,7 +811,9 @@ function InstanceCard({ instanceId, instanceName, isActive, onSelect }: Instance
? t('taskList.stopTasks')
: canRun
? t('taskList.startTasks')
: t('dashboard.noEnabledTasks')
: runPreview.skippedByWeeklySchedule.length > 0
? t('taskList.allTasksSkippedByWeeklySchedule')
: t('dashboard.noEnabledTasks')
}
>
{isStarting || isStopping ? (
Expand Down
153 changes: 139 additions & 14 deletions src/components/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AlertCircle,
Play,
CircleDot,
CalendarDays,
} from 'lucide-react';
import { useAppStore, type TaskRunStatus } from '@/stores/appStore';
import { maaService } from '@/services/maaService';
Expand All @@ -30,10 +31,15 @@ import { isMxuSpecialTask, getMxuSpecialTask, findMxuOptionByKey } from '@/types
import { isPretaskName, getPretaskItem, buildPretaskDef } from '@/types/pretasks';
import { getInterfaceLangKey } from '@/i18n';
import { stripInlineRichText } from '@/utils/richText';
import { isTaskBlockedByWeeklySchedule, normalizeWeeklySchedule } from '@/utils/taskRunFilter';
import { useCurrentWeekday } from '@/hooks/useCurrentWeekday';
import clsx from 'clsx';
import { loggers } from '@/utils/logger';
import { isPasswordInput } from '@/utils/passwordOptionValues';

/** 周计划的全部星期(0=周日);"启用周计划"时默认全选,启用后行为与启用前一致,再由用户取消不需要的天 */
const ALL_WEEKDAYS = [0, 1, 2, 3, 4, 5, 6];

/** 选项预览标签组件 */
function OptionPreviewTag({
label,
Expand Down Expand Up @@ -324,6 +330,7 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
projectInterface,
toggleTaskEnabled,
setTaskRunOnce,
setTaskWeeklySchedule,
toggleTaskExpanded,
removeTaskFromInstance,
confirmBeforeDelete,
Expand All @@ -338,6 +345,7 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
getActiveInstance,
showOptionPreview,
instanceTaskRunStatus,
instanceTaskRunSelection,
instances,
findMaaTaskIdBySelectedTaskId,
basePath,
Expand All @@ -346,12 +354,29 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
removeAnimatingTaskId,
} = useAppStore();

// 获取任务运行状态
const taskRunStatus: TaskRunStatus = instanceTaskRunStatus[instanceId]?.[task.id] || 'idle';

// 获取实例运行状态
const instance = instances.find((i) => i.id === instanceId);
const isInstanceRunning = instance?.isRunning || false;
const todayWeekday = useCurrentWeekday();
const runSelection = instanceTaskRunSelection[instanceId];
const backendTaskRunStatus: TaskRunStatus =
instanceTaskRunStatus[instanceId]?.[task.id] || 'idle';
// 后端正在排队/执行的事实优先;刷新页面恢复的批次可能没有本地筛选快照。
const hasCurrentExecution =
backendTaskRunStatus === 'pending' || backendTaskRunStatus === 'running';
const weeklyScheduleBlockedToday =
!hasCurrentExecution &&
(runSelection
? runSelection.excludedByWeeklyScheduleIds.includes(task.id)
: !(isInstanceRunning && backendTaskRunStatus !== 'idle') &&
isTaskBlockedByWeeklySchedule(task, todayWeekday));
// 历史成功/失败不能冒充本次结果;显式单独运行、跨午夜时以启动快照为准。
const taskRunStatus: TaskRunStatus =
!hasCurrentExecution &&
(weeklyScheduleBlockedToday ||
(runSelection && !runSelection.includedTaskIds.includes(task.id)))
? 'idle'
: backendTaskRunStatus;

// 获取任务定义 - 支持 MXU 内置特殊任务与 pretask 前置任务
const isMxuTask = isMxuSpecialTask(task.taskName);
Expand Down Expand Up @@ -418,7 +443,10 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
]);

// 紧凑模式:实例运行时,未参与运行的任务显示为紧凑样式
const isCompact = isInstanceRunning && !task.enabled && !task.runOnce && taskRunStatus === 'idle';
const isCompact =
isInstanceRunning &&
(weeklyScheduleBlockedToday || (!task.enabled && !task.runOnce)) &&
taskRunStatus === 'idle';

// 判断是否可以编辑选项:实例未运行时始终可以编辑,运行中只有 pending 或 idle 状态的任务可以编辑
const canEditOptions =
Expand Down Expand Up @@ -550,8 +578,15 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
const hasOptions = !!taskDef?.option && taskDef.option.length > 0;
// 判断是否有描述内容(包括正在加载的情况)
const hasDescription = !!resolvedDescription.html || resolvedDescription.loading;
// 有选项或有描述时都可以展开
const canExpand = hasOptions || hasDescription;
// 周计划:0-6(0=周日);缺省或空数组表示未启用
const weeklySchedule = useMemo(
() => normalizeWeeklySchedule(task.weeklySchedule) ?? [],
[task.weeklySchedule],
);
const hasWeeklySchedule = weeklySchedule.length > 0;
const weekdayLabels = t('schedule.weekdays', { returnObjects: true }) as string[];
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
// 有选项、有描述或启用了周计划时都可以展开
const canExpand = hasOptions || hasDescription || hasWeeklySchedule;

// 生成选项预览信息(最多显示3个)
const optionPreviews = useMemo(() => {
Expand Down Expand Up @@ -651,6 +686,16 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {

const checkboxState = getTaskCheckboxState(task.enabled, Boolean(task.runOnce));

const handleToggleWeeklyScheduleDay = useCallback(
(day: number) => {
const next = weeklySchedule.includes(day)
? weeklySchedule.filter((d) => d !== day)
: [...weeklySchedule, day];
setTaskWeeklySchedule(instanceId, task.id, next);
},
[weeklySchedule, instanceId, task.id, setTaskWeeklySchedule],
);

const handleCheckboxClick = () => {
if (isInstanceRunning || isIncompatible) return;
toggleTaskEnabled(instanceId, task.id);
Expand All @@ -676,18 +721,36 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
disabled: !task.runOnce,
onClick: () => setTaskRunOnce(instanceId, task.id, false),
},
{
id: 'weekly-schedule',
label: t('contextMenu.enableWeeklySchedule'),
icon: CalendarDays,
checked: hasWeeklySchedule,
onClick: () => {
const nextEnabled = !hasWeeklySchedule;
setTaskWeeklySchedule(instanceId, task.id, nextEnabled ? ALL_WEEKDAYS : []);
// 启用后展开卡片,否则收起状态下看不到周计划区域、没有任何反馈
if (nextEnabled && !task.expanded) {
toggleTaskExpanded(instanceId, task.id);
}
},
},
];

showMenu(e, menuItems);
},
[
t,
task.runOnce,
task.expanded,
hasWeeklySchedule,
instanceId,
task.id,
isInstanceRunning,
isIncompatible,
setTaskRunOnce,
setTaskWeeklySchedule,
toggleTaskExpanded,
showMenu,
],
);
Expand Down Expand Up @@ -915,9 +978,11 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
title={
isIncompatible
? incompatibleReason
: checkboxState === 'once'
? t('taskItem.runOnceHint')
: undefined
: weeklyScheduleBlockedToday
? t('taskItem.weeklyScheduleSkippedTodayHint')
: checkboxState === 'once'
? t('taskItem.runOnceHint')
: undefined
}
>
<TriStateCheckbox
Expand Down Expand Up @@ -956,11 +1021,13 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
<span
className={clsx(
'min-w-0 text-sm font-medium truncate',
task.enabled
? 'text-text-primary'
: task.runOnce
? 'text-accent'
: 'text-text-muted',
weeklyScheduleBlockedToday
? 'text-text-muted'
: task.enabled
? 'text-text-primary'
: task.runOnce
? 'text-accent'
: 'text-text-muted',
)}
>
{displayName}
Expand All @@ -972,6 +1039,17 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
)}
</div>

{weeklyScheduleBlockedToday && (
<span
data-role="weekly-schedule-exclusion"
className="ml-2 inline-flex items-center gap-1 shrink-0 rounded bg-bg-hover px-1.5 py-0.5 text-xs text-text-muted"
title={t('taskItem.weeklyScheduleSkippedTodayHint')}
>
<CalendarDays className="h-3 w-3" />
{t('taskItem.weeklyScheduleSkippedToday')}
</span>
)}

{/* 不带选项的任务:直接显示不兼容警告 */}
{!canExpand && isIncompatible && (
<div className="flex-1 flex items-center gap-1.5 mx-2 overflow-hidden">
Expand Down Expand Up @@ -1085,6 +1163,53 @@ export function TaskItem({ instanceId, task }: TaskItemProps) {
</div>
</Tooltip>
)}
{/* 周计划区域 - 仅在启用时显示 */}
{hasWeeklySchedule && (
<div
className={clsx(
'rounded-md border border-border bg-bg-primary p-2.5',
(hasOptions || isIncompatible) && 'mb-3',
)}
>
<div className="flex items-center gap-1.5 mb-2">
<CalendarDays className="w-3.5 h-3.5 text-text-muted flex-shrink-0" />
<span className="text-xs font-medium text-text-secondary">
{t('taskItem.weeklyScheduleTitle')}
</span>
{weeklyScheduleBlockedToday && (
<span className="text-xs text-text-muted">
· {t('taskItem.weeklyScheduleSkippedToday')}
</span>
)}
</div>
<div className="flex flex-wrap gap-1">
{[1, 2, 3, 4, 5, 6, 0].map((day) => {
const selected = weeklySchedule.includes(day);
const isToday = day === todayWeekday;
const dayDisabled = isInstanceRunning || isIncompatible;
return (
<button
key={day}
type="button"
onClick={() => handleToggleWeeklyScheduleDay(day)}
disabled={dayDisabled}
title={isToday ? t('taskItem.weeklyScheduleToday') : undefined}
className={clsx(
'px-2 py-1 text-xs rounded border transition-colors',
selected
? 'bg-accent text-white border-accent'
: 'bg-bg-primary text-text-secondary border-border hover:border-accent hover:text-accent',
isToday && !selected && 'ring-1 ring-accent/40',
dayDisabled && 'opacity-50 cursor-not-allowed',
)}
>
{weekdayLabels[day]}
</button>
);
})}
</div>
</div>
)}
{/* 选项列表 - 仅在有选项时显示 */}
{hasOptions && (
<OptionListRenderer
Expand Down
Loading
Loading