mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
Three layers landed together because they share the same routing /
lifecycle plumbing:
1. Cron output unification
- New CronConversationResolver routes web-origin jobs to the per-workspace
tasks_<wsId> conversation; IM-bound jobs go to the channel session
conversation when one exists (matched by senderId then targetId);
legacy cron_<id> remains as the fallback.
- CronJobLifecycleService inserts a system-role header divider when a
run starts so users browsing the unified tasks_<wsId> view can tell
which job started a run. BaseAgent.sanitizeForLlm filters these
headers so they never reach the model.
- WorkspaceService seeds tasks_<wsId> on workspace creation; V65
migration backfills existing workspaces.
- DeliveryConfig gains a userId field so IM session lookup can match
by senderId (replyToken-based targetId is not stable across runs).
- ConversationVO recognizes tasks_/cron_ underscore prefix as cron
source. MessageList renders the system header as a labeled divider.
- ChatConsole pins tasks_* conversations and tracks per-conversation
read state so new cron output gets a visible unread dot.
2. Reminder task type
- New task_type='reminder' in CronJobEntity + service validation.
- CronJobRunner short-circuits 'reminder' jobs: hands trigger_message
to finishRunAndPublish verbatim, no LLM call. Fixes a regression
where reminders were rephrased into echoed wrappers.
- New create_reminder tool alongside create_cron_job, with descriptions
tightened so the model picks the right one (verbatim push vs LLM
query that needs computation).
- CronJobs.vue gets a third radio option + dedicated reminder field.
3. In-flight progress placeholder
- Cron uses non-streaming chat()/execute(); tool-heavy ReAct loops
can run 1-5 minutes between start and finish with no visible
state, looking hung.
- New GET /api/v1/cron-jobs/active-runs returns runs in status=running
for a conversation. ChatConsole polls it on the existing 4s tick
(and on conversation switch) and shows a spinner bar with elapsed
time. When run count drops to zero, it refetches messages so the
assistant bubble appears within ~1s of finish.
116 lines
4.4 KiB
Java
116 lines
4.4 KiB
Java
package vip.mate.cron.controller;
|
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.web.bind.annotation.*;
|
|
import vip.mate.common.result.R;
|
|
import vip.mate.cron.model.CronJobDTO;
|
|
import vip.mate.cron.service.CronJobService;
|
|
import vip.mate.dashboard.model.ActiveCronRunVO;
|
|
import vip.mate.dashboard.service.CronJobRunService;
|
|
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* 定时任务管理接口
|
|
*
|
|
* @author MateClaw Team
|
|
*/
|
|
@Tag(name = "定时任务管理")
|
|
@RestController
|
|
@RequestMapping("/api/v1/cron-jobs")
|
|
@RequiredArgsConstructor
|
|
public class CronJobController {
|
|
|
|
private final CronJobService cronJobService;
|
|
private final CronJobRunService cronJobRunService;
|
|
|
|
/**
|
|
* RFC-083: every endpoint reads {@code X-Workspace-Id} (the frontend
|
|
* axios interceptor already injects it). Service-layer filtering is
|
|
* required because {@link vip.mate.config.WorkspaceAccessInterceptor}
|
|
* skips its membership check entirely for global {@code admin} users —
|
|
* relying on the interceptor alone leaks cron jobs across workspaces.
|
|
*/
|
|
private static final long DEFAULT_WORKSPACE_ID = 1L;
|
|
|
|
@Operation(summary = "获取定时任务列表")
|
|
@GetMapping
|
|
@RequireWorkspaceRole("viewer")
|
|
public R<List<CronJobDTO>> list(
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
return R.ok(cronJobService.list(resolve(workspaceId)));
|
|
}
|
|
|
|
@Operation(summary = "获取定时任务详情")
|
|
@GetMapping("/{id}")
|
|
@RequireWorkspaceRole("viewer")
|
|
public R<CronJobDTO> get(@PathVariable Long id,
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
return R.ok(cronJobService.getById(id, resolve(workspaceId)));
|
|
}
|
|
|
|
@Operation(summary = "创建定时任务")
|
|
@PostMapping
|
|
@RequireWorkspaceRole("member")
|
|
public R<CronJobDTO> create(@RequestBody CronJobDTO dto,
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
return R.ok(cronJobService.create(dto, resolve(workspaceId)));
|
|
}
|
|
|
|
@Operation(summary = "更新定时任务")
|
|
@PutMapping("/{id}")
|
|
@RequireWorkspaceRole("member")
|
|
public R<CronJobDTO> update(@PathVariable Long id, @RequestBody CronJobDTO dto,
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
return R.ok(cronJobService.update(id, dto, resolve(workspaceId)));
|
|
}
|
|
|
|
@Operation(summary = "删除定时任务")
|
|
@DeleteMapping("/{id}")
|
|
@RequireWorkspaceRole("admin")
|
|
public R<Void> delete(@PathVariable Long id,
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
cronJobService.delete(id, resolve(workspaceId));
|
|
return R.ok();
|
|
}
|
|
|
|
@Operation(summary = "启用/禁用定时任务")
|
|
@PutMapping("/{id}/toggle")
|
|
@RequireWorkspaceRole("member")
|
|
public R<Void> toggle(@PathVariable Long id, @RequestParam boolean enabled,
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
cronJobService.toggle(id, enabled, resolve(workspaceId));
|
|
return R.ok();
|
|
}
|
|
|
|
@Operation(summary = "立即执行定时任务")
|
|
@PostMapping("/{id}/run")
|
|
@RequireWorkspaceRole("member")
|
|
public R<Void> runNow(@PathVariable Long id,
|
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
|
cronJobService.runNow(id, resolve(workspaceId));
|
|
return R.ok();
|
|
}
|
|
|
|
/**
|
|
* Lightweight poll target for the chat console. Returns the cron job runs
|
|
* currently in {@code running} state for the given conversation, so the
|
|
* UI can render a "executing…" placeholder bubble between T1 (run row
|
|
* inserted) and T2 (assistant message persisted).
|
|
*/
|
|
@Operation(summary = "查询会话下正在执行的定时任务运行")
|
|
@GetMapping("/active-runs")
|
|
@RequireWorkspaceRole("viewer")
|
|
public R<List<ActiveCronRunVO>> activeRuns(
|
|
@RequestParam("conversationId") String conversationId) {
|
|
return R.ok(cronJobRunService.listActiveByConversation(conversationId));
|
|
}
|
|
|
|
private static long resolve(Long headerWorkspaceId) {
|
|
return headerWorkspaceId != null ? headerWorkspaceId : DEFAULT_WORKSPACE_ID;
|
|
}
|
|
}
|