事件触发的规模:定时任务列表里每个事件触发任务「有几条在等」一次查齐(outbox.pending_for_many:待派发数一条 SQL 按任务分组算,窗口要看的事件一次读出),不再每行查一次;每一拍派发只读触发条件的事件种类、对象对得上的任务(SQLAlchemy 的通用 JSON 访问),保存时查环一层查一次、只顺着「上游工作流运行成功结束」的边找;选对象(trigger-options)按事件、关键字查、一页最多 100 个、more 说还有没有,当前选中的总在里面,编辑页一页没列完时多一个按名称查找。顺带修:等着的时候任务改了条件、最早那条不再合条件时,「什么时候合成触发」按剩下的最早一条算(列表上的时刻和真发起的一致);一个窗口的结束时刻和这个任务已有的另一次触发撞在同一刻(别的期次早一拍发了)时往后挪,那几条事件照常发起,不再被记到已有的触发上

This commit is contained in:
ThearchyHelios
2026-10-19 09:46:47 +08:00
parent fa431da91b
commit 358206312e
9 changed files with 451 additions and 66 deletions
+6 -2
View File
@@ -36,8 +36,12 @@ export const schedules = {
*/
eventVariables: (event: ScheduleEventKind, opts?: RequestOptions) =>
get<ScheduleVariable[]>(`/schedules/variables${qs({ trigger_kind: 'event', event })}`, opts),
/** 新建、修改事件触发任务时能选的对象(能不能选、为什么由服务端判)、来源、应用。老服务端没有,回 404 */
triggerOptions: (opts?: RequestOptions) => get<ScheduleTriggerOptions>('/schedules/trigger-options', opts),
/**
* 新建、修改事件触发任务时能选的对象(能不能选、为什么由服务端判)、来源、应用。老服务端没有,回 404。对象一页有上限
* (more 为真时没给完):按事件种类、名字里的关键字再查;selected 是当前选中的那一个,总在结果里
*/
triggerOptions: (find?: { event?: ScheduleEventKind; q?: string; selected?: string }, opts?: RequestOptions) =>
get<ScheduleTriggerOptions>(`/schedules/trigger-options${qs(find)}`, opts),
/** 立即运行一次:记一条手动触发,上一次还没结束时照样按重叠策略跳过 */
run: (id: string) => post<ScheduleRunResult>(`${at(id)}/run`),
/** 触发历史,新的在前。before 是上一页给的 next_before */
+4
View File
@@ -43,6 +43,10 @@ export const TRIGGER_TEXT = {
/** 不能选的对象:名字后面接服务端给的原因 */
subjectNotOk: (name: string, reason: string) => `${name}(${reason})`,
subjectGone: (name: string) => `${name}(已不存在)`,
/** 对象太多、一页没列完:按名字查找 */
subjectSearch: (label: string) => `按名称查找${label}`,
subjectSearchHint: '只列出了一部分,输入名称中的字查找',
subjectSearchNone: '没有名称中含这些字的',
optionsLoading: '正在读取可选的对象…',
optionsFailed: '无法读取可选的对象',
optionsUnsupported: '服务端暂不支持事件触发,请升级服务端后再使用',
+39 -2
View File
@@ -73,6 +73,28 @@ function useTriggerOptions(): OptionsState {
return state
}
/**
* 对象太多、一页没列完(服务端的 more)时按名字查:输入停一下再查,结果只换这一种事件的对象;当前选中的那一个总带上
* (selected),不被分页挤掉、不显示成已不存在
*/
function useObjectSearch(kind: ScheduleEventKind, selected: string, enabled: boolean) {
const [q, setQ] = useState('')
const [found, setFound] = useState<{ key: string; objects: ScheduleTriggerOptions['events'][number]['objects'] } | null>(null)
const words = q.trim()
const key = `${kind}\u0000${words}`
useEffect(() => {
if (!enabled || !words) return
const controller = new AbortController()
const timer = setTimeout(() => {
api.schedules.triggerOptions({ event: kind, q: words, selected: selected || undefined }, { signal: controller.signal })
.then((data) => setFound({ key, objects: data.events.find((e) => e.event === kind)?.objects ?? [] }))
.catch(() => {})
}, 300)
return () => { clearTimeout(timer); controller.abort() }
}, [enabled, kind, words, selected, key])
return { q, setQ, objects: words && found?.key === key ? found.objects : null }
}
export function EventSection({ value, onChange, timezone, setTimezone, savedName, error, debounceError }: {
value: TriggerForm
onChange: (t: TriggerForm) => void
@@ -91,7 +113,9 @@ export function EventSection({ value, onChange, timezone, setTimezone, savedName
const kind = value.event
const label = TT.subject[kind]
const entry = options.status === 'ok' ? options.data.events.find((e) => e.event === kind) : undefined
const objects = entry?.objects ?? []
const paged = !!entry?.more
const search = useObjectSearch(kind, value.subjectId, paged)
const objects = search.objects ?? entry?.objects ?? []
const listed = objects.some((o) => o.id === value.subjectId)
let picker
@@ -112,7 +136,10 @@ export function EventSection({ value, onChange, timezone, setTimezone, savedName
aria-describedby={error ? 'schedule-trigger-subject-error' : undefined}
onChange={(e) => set({ subjectId: e.target.value })}>
<option value="">{objects.length ? TT.subjectPlaceholder(label) : TT.subjectEmpty[kind]}</option>
{value.subjectId && !listed && <option value={value.subjectId}>{TT.subjectGone(savedName || value.subjectId)}</option>}
{/* 一页没列完时不在列表里不等于不存在:写保存时记下的名字,不标「已不存在」 */}
{value.subjectId && !listed && (
<option value={value.subjectId}>{paged ? savedName || value.subjectId : TT.subjectGone(savedName || value.subjectId)}</option>
)}
{objects.map((o) => (
<option key={o.id} value={o.id} disabled={!o.ok && o.id !== value.subjectId} data-trigger-object-ok={String(o.ok)}>
{o.ok || !o.reason ? o.name : TT.subjectNotOk(o.name, o.reason)}
@@ -140,7 +167,17 @@ export function EventSection({ value, onChange, timezone, setTimezone, savedName
</div>
<Field label={label} htmlFor="schedule-trigger-subject" required>
{paged && (
<input type="search" className="field mb-1.5" value={search.q} data-trigger-subject-search=""
placeholder={TT.subjectSearch(label)} aria-label={TT.subjectSearch(label)}
aria-describedby="schedule-trigger-subject-search-hint" onChange={(e) => search.setQ(e.target.value)} />
)}
{picker}
{paged && (
<p id="schedule-trigger-subject-search-hint" className="mt-1 text-2xs leading-relaxed text-faint">
{search.objects && !search.objects.length ? TT.subjectSearchNone : TT.subjectSearchHint}
</p>
)}
</Field>
{error && (
<div id="schedule-trigger-subject-error" className="-mt-1.5 text-2xs leading-relaxed text-[var(--err)]" role="alert"
+2
View File
@@ -4172,6 +4172,8 @@ export interface ScheduleTriggerOptions {
label: string
subject_label: string
objects: { id: string; name: string; ok: boolean; reason: string | null }[]
/** 一页没给完(对象按名字排、一页有上限):界面按关键字再查。老服务端不给,当作给完了 */
more?: boolean
}[]
origins: { kind: string; label: string }[]
apps: { id: string; name: string }[]