69 lines
3.1 KiB
Svelte
69 lines
3.1 KiB
Svelte
<script>
|
|
import { onMount } from "svelte";
|
|
import { Tabs } from "bits-ui";
|
|
import { api, formatTime } from "../lib/api.js";
|
|
import StatusBadge from "../lib/StatusBadge.svelte";
|
|
|
|
let tab = $state("scheduled");
|
|
let jobs = $state([]);
|
|
let tasks = $state([]);
|
|
let runs = $state({});
|
|
let loading = $state(true);
|
|
let error = $state("");
|
|
|
|
async function load() {
|
|
loading = true;
|
|
error = "";
|
|
try {
|
|
if (tab === "background") {
|
|
tasks = (await api("/api/tasks?limit=200")).tasks;
|
|
} else {
|
|
jobs = (await api("/api/jobs")).jobs;
|
|
runs = Object.fromEntries(await Promise.all(jobs.map(async (job) => [
|
|
job.id,
|
|
await api(`/api/jobs/${encodeURIComponent(job.id)}/runs?limit=5`).then((value) => value.runs).catch(() => [])
|
|
])));
|
|
}
|
|
} catch (caught) {
|
|
error = caught.message;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function changeTab(value) {
|
|
tab = value;
|
|
load();
|
|
}
|
|
|
|
onMount(load);
|
|
</script>
|
|
|
|
<section class="page active content-page">
|
|
<div class="toolbar">
|
|
<Tabs.Root value={tab} onValueChange={changeTab}>
|
|
<Tabs.List class="tabs" aria-label="任务类型">
|
|
<Tabs.Trigger value="scheduled">定时任务</Tabs.Trigger>
|
|
<Tabs.Trigger value="background">后台任务</Tabs.Trigger>
|
|
</Tabs.List>
|
|
</Tabs.Root>
|
|
<button class="secondary" onclick={load}>↻ 刷新</button>
|
|
</div>
|
|
<div class="cards">
|
|
{#if loading}<div class="loading">加载中…</div>
|
|
{:else if error}<div class="empty-card error-text">{error}</div>
|
|
{:else if tab === "background"}
|
|
{#each tasks as task (task.id)}
|
|
<article class="card"><div class="card-row"><div><h3>{task.prompt.slice(0, 100)}</h3><div class="meta"><span>{task.session_id}</span><span>{formatTime(task.created_at)}</span><span>{task.tool_calls_count} 次工具调用 · {task.iterations} 轮</span></div></div><StatusBadge status={task.status} /></div>{#if task.result}<div class="details"><p>{task.result}</p></div>{/if}{#if task.error}<p class="error-text">{task.error}</p>{/if}</article>
|
|
{:else}<div class="empty-card">暂无后台任务</div>{/each}
|
|
{:else}
|
|
{#each jobs as job (job.id)}
|
|
<article class="card">
|
|
<div class="card-row"><div><h3>{job.name}</h3><p>{job.prompt}</p><div class="meta"><span>{job.job_kind === "monitor" ? "巡检" : "任务"} · {job.delivery_policy}</span><span>{job.channel} · {job.chat_id}</span><span>下次 {formatTime(job.next_run_at)}</span><span>上次 {formatTime(job.last_run_at)}</span></div></div><StatusBadge status={job.enabled ? (job.last_status || "enabled") : "disabled"} /></div>
|
|
{#if runs[job.id]?.length}<div class="details">{#each runs[job.id] as run}<div class="card-row run-row"><span class="meta">{formatTime(run.finished_at)} · {run.duration_ms}ms{run.result_kind ? ` · ${run.result_kind}` : ""}{run.delivery_status ? ` · ${run.delivery_status}` : ""}</span><StatusBadge status={run.status} /></div>{/each}</div>{/if}
|
|
</article>
|
|
{:else}<div class="empty-card">暂无定时任务</div>{/each}
|
|
{/if}
|
|
</div>
|
|
</section>
|