mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-16 13:43:17 +08:00
Inherit agent presets and auto-bind IM delivery targets (0.1.7).
Cron jobs created from WhatsApp can omit bot/target ids and reuse chat overrides; sidebar/agent preset pins persist on fire. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2f08136adf
commit
2e690c30c8
15 changed files with 830 additions and 82 deletions
216
lib/client.js
216
lib/client.js
|
|
@ -161,8 +161,13 @@ window.__ModuleLoader__.load({
|
|||
const inject = ['slots', 'locale', 'settingsScope']
|
||||
const API = '/dsh-ops-cron'
|
||||
const LOCALE_NS = 'settings.dshCronTasks'
|
||||
const TITLE_PREFIX = '运维定时 · '
|
||||
const listSnapshot = { jobs: [], runs: [], workspaces: [], catalog: { groups: [], current: null } }
|
||||
const TITLE_PREFIX = '定时任务 · '
|
||||
const listSnapshot = {
|
||||
jobs: [], runs: [], workspaces: [],
|
||||
catalog: { groups: [], current: null },
|
||||
presets: { items: [], current: null },
|
||||
imCatalog: { available: true, options: [], loading: false },
|
||||
}
|
||||
|
||||
function jobStamp(rows) {
|
||||
return (rows || []).map((row) => `${row.id}:${row.updatedAt}:${row.nextRunAt}:${row.enabled}:${row.lastStatus}:${row.model}:${row.provider}:${row.delivery?.kind}:${row.delivery?.targetId}`).join('|')
|
||||
|
|
@ -291,19 +296,19 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
`
|
||||
|
||||
const zh = {
|
||||
title: '运维定时',
|
||||
title: '定时任务',
|
||||
description: '调度策略:启用、默认时区、历史保留、重叠与漏跑。',
|
||||
expand: '展开设置', collapse: '收起设置', save: '保存', saving: '保存中…', discard: '放弃修改',
|
||||
saveFailed: '本部署没有接受这些值,已保留供你修改。',
|
||||
enabled: '启用调度器', timezone: '默认时区', historyLimit: '历史保留条数',
|
||||
overlapPolicy: '重叠策略', misfirePolicy: '漏跑策略', policySkip: '跳过',
|
||||
hint: '任务在左侧「新会话」下方进入。运行结果会出现在历史行里,点击记录会打开和普通会话一样的对话页,可以继续聊。',
|
||||
entry: '运维定时', entryLabel: '打开运维定时',
|
||||
entry: '定时任务', entryLabel: '打开定时任务',
|
||||
backToWorkspace: '返回工作区', backLabel: '返回工作区',
|
||||
newJob: '新建任务', name: '名称', prompt: '提示词', kind: '日程',
|
||||
cron: 'Cron(循环)', at: '一次性时间', expr: 'Cron 表达式', atTime: '时间',
|
||||
create: '创建', pause: '暂停', resume: '恢复', runNow: '立即运行', remove: '删除',
|
||||
emptyJobs: '还没有运维定时。点右上角 + 新建。', next: '下次', last: '上次',
|
||||
emptyJobs: '还没有定时任务。点右上角 + 新建。', next: '下次', last: '上次',
|
||||
expandRuns: '展开记录', collapseRuns: '收起记录',
|
||||
search: '搜索', searchPlaceholder: '搜索任务', searchClear: '清除搜索', searchEmpty: '没有匹配的任务。',
|
||||
paused: '已暂停',
|
||||
|
|
@ -313,7 +318,10 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
cwdHint: '运行会在这个目录对应的工作区里开新会话。留空则用最近工作区。',
|
||||
model: '模型',
|
||||
modelDefault: '每次运行用当时的新会话默认',
|
||||
modelHint: '运维定时会消耗这个模型的额度。指定后不会跟着聊天模型变。',
|
||||
modelHint: '定时任务会消耗这个模型的额度。指定后不会跟着聊天模型变。',
|
||||
agentPreset: 'Agent Preset',
|
||||
agentPresetDefault: '每次运行用当时的 Host 默认',
|
||||
agentPresetHint: '指定后每次触发都挂载该 Preset;留空则跟随 Host 默认(创建时若从 WhatsApp 继承会自动写入)。',
|
||||
editorLead: '保存后按日程在新会话里执行;若投递为 IM,结束后还会发摘要到 WhatsApp/IM。点击下方某次运行,右侧会打开那次对话,可以继续。',
|
||||
lastOutput: '上次输出', noOutput: '还没有输出。先立即运行一次。',
|
||||
delivery: '投递',
|
||||
|
|
@ -321,17 +329,22 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
deliveryIm: 'WhatsApp / IM',
|
||||
imBotId: 'IM Bot ID',
|
||||
imTargetId: 'IM Target ID',
|
||||
deliveryHint: 'Web 默认 DSH;WhatsApp 会话里由 Agent 创建时可自动匹配投递目标。也可粘贴 IM「投递设置」里的 botId/targetId。',
|
||||
imTarget: '投递目标',
|
||||
imTargetNone: '(请先在 IM「投递设置」新建目标)',
|
||||
imTargetManual: '手动填写…',
|
||||
imCatalogLoading: '正在加载投递目标…',
|
||||
imCatalogUnavailable: '无法加载投递目标(需 dsh-im-ops ≥ops.24,且已配置投递目标)',
|
||||
deliveryHint: '选 WhatsApp/IM 后从下拉选择已保存的投递目标;也可手动填写。目标在 IM 机器人 → 投递设置里创建。',
|
||||
}
|
||||
const en = {
|
||||
title: 'Ops schedule',
|
||||
title: 'Scheduled tasks',
|
||||
description: 'Scheduler policy: enable, default time zone, history retention, overlap and misfire.',
|
||||
expand: 'Show settings', collapse: 'Hide settings', save: 'Save', saving: 'Saving…', discard: 'Discard',
|
||||
saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
|
||||
enabled: 'Enable scheduler', timezone: 'Default time zone', historyLimit: 'History retention',
|
||||
overlapPolicy: 'Overlap policy', misfirePolicy: 'Misfire policy', policySkip: 'Skip',
|
||||
hint: 'Open from under New Session. Run output appears in history; click a run to continue in the normal chat view.',
|
||||
entry: 'Ops schedule', entryLabel: 'Open ops schedule',
|
||||
entry: 'Scheduled tasks', entryLabel: 'Open scheduled tasks',
|
||||
backToWorkspace: 'Back to workspace', backLabel: 'Back to workspace',
|
||||
newJob: 'New job', name: 'Name', prompt: 'Prompt', kind: 'Schedule',
|
||||
cron: 'Cron (recurring)', at: 'One-shot time', expr: 'Cron expression', atTime: 'Time',
|
||||
|
|
@ -347,6 +360,9 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
model: 'Model',
|
||||
modelDefault: 'Use the New Session default at fire time',
|
||||
modelHint: 'Scheduled runs consume this model\'s quota. A pinned model will not follow the chat selector.',
|
||||
agentPreset: 'Agent preset',
|
||||
agentPresetDefault: 'Use the Host default at fire time',
|
||||
agentPresetHint: 'Pin a preset for every fire. Leave empty to follow the Host default (WhatsApp-created jobs inherit the chat preset automatically).',
|
||||
editorLead: 'Runs start a new session. IM delivery also sends a summary via dshIm after the turn. Click a run below to open that conversation.',
|
||||
lastOutput: 'Last output', noOutput: 'No output yet. Run it once.',
|
||||
delivery: 'Delivery',
|
||||
|
|
@ -354,7 +370,12 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
deliveryIm: 'WhatsApp / IM',
|
||||
imBotId: 'IM Bot ID',
|
||||
imTargetId: 'IM Target ID',
|
||||
deliveryHint: 'Web defaults to DSH. WhatsApp sessions can auto-match a delivery target. Or paste botId/targetId from IM delivery settings.',
|
||||
imTarget: 'Delivery target',
|
||||
imTargetNone: '(Create a target in IM delivery settings first)',
|
||||
imTargetManual: 'Enter manually…',
|
||||
imCatalogLoading: 'Loading delivery targets…',
|
||||
imCatalogUnavailable: 'Cannot load targets (need dsh-im-ops ≥ops.24 with saved targets)',
|
||||
deliveryHint: 'Pick a saved IM delivery target from the list, or enter botId/targetId manually. Create targets under IM bot → Delivery settings.',
|
||||
}
|
||||
|
||||
function normalizePrefs(value) {
|
||||
|
|
@ -597,6 +618,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
timezone: timezone || DEFAULTS.timezone, cwd: '', timeoutMinutes: 10, enabled: true,
|
||||
provider: current?.provider || '',
|
||||
model: current?.model || '',
|
||||
agentPreset: '',
|
||||
deliveryKind: 'dsh',
|
||||
imBotId: '',
|
||||
imTargetId: '',
|
||||
|
|
@ -616,6 +638,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
enabled: job.enabled !== false,
|
||||
provider: job.provider || '',
|
||||
model: job.model || '',
|
||||
agentPreset: job.agentPreset || '',
|
||||
deliveryKind: job.delivery?.kind === 'im' ? 'im' : 'dsh',
|
||||
imBotId: job.delivery?.botId || '',
|
||||
imTargetId: job.delivery?.targetId || '',
|
||||
|
|
@ -858,7 +881,110 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
)
|
||||
}
|
||||
|
||||
function JobEditor({ t, form, setForm, isNew, error, lastOutput, nextRunAt, lastRunAt, workspaces, catalog, onSave, onRun, onPause, onRemove }) {
|
||||
function PresetField({ t, form, setForm, presets }) {
|
||||
const items = Array.isArray(presets?.items) ? presets.items : []
|
||||
const current = presets?.current
|
||||
const selected = form.agentPreset || ''
|
||||
const known = items.some((row) => row.id === selected)
|
||||
const defaultLabel = current?.id
|
||||
? `${t('agentPresetDefault')}(${current.name || current.id})`
|
||||
: t('agentPresetDefault')
|
||||
return h('label', null, t('agentPreset'),
|
||||
h('select', {
|
||||
value: selected,
|
||||
onChange: (e) => setForm({ ...form, agentPreset: e.target.value }),
|
||||
},
|
||||
h('option', { value: '' }, defaultLabel),
|
||||
!known && selected
|
||||
? h('option', { value: selected }, selected)
|
||||
: null,
|
||||
...items.map((row) => h('option', {
|
||||
value: row.id,
|
||||
key: row.id,
|
||||
}, row.name || row.id)),
|
||||
),
|
||||
h('span', { className: 'dsh-ct-cwdHint' }, t('agentPresetHint')),
|
||||
)
|
||||
}
|
||||
|
||||
function imOptionKey(botId, targetId) {
|
||||
return `${botId || ''}::${targetId || ''}`
|
||||
}
|
||||
|
||||
function parseImOptionKey(value) {
|
||||
const raw = String(value || '')
|
||||
const at = raw.indexOf('::')
|
||||
if (at <= 0) return { botId: '', targetId: '' }
|
||||
return { botId: raw.slice(0, at), targetId: raw.slice(at + 2) }
|
||||
}
|
||||
|
||||
function ImDeliveryFields({ t, form, setForm, imCatalog }) {
|
||||
const options = Array.isArray(imCatalog?.options) ? imCatalog.options : []
|
||||
const loading = !!imCatalog?.loading
|
||||
const available = imCatalog?.available !== false
|
||||
const selectedKey = form.imBotId && form.imTargetId
|
||||
? imOptionKey(form.imBotId, form.imTargetId)
|
||||
: ''
|
||||
const matched = options.some((row) => imOptionKey(row.botId, row.targetId) === selectedKey)
|
||||
const [manual, setManual] = useState(() => !!(selectedKey && !matched))
|
||||
useEffect(() => {
|
||||
if (selectedKey && !matched && !loading) setManual(true)
|
||||
if (selectedKey && matched) setManual(false)
|
||||
}, [selectedKey, matched, loading])
|
||||
const selectValue = manual
|
||||
? '__manual__'
|
||||
: (matched ? selectedKey : '')
|
||||
const showManual = manual || (selectedKey && !matched && !loading)
|
||||
|
||||
return h(React.Fragment, null,
|
||||
h('label', null, t('imTarget'),
|
||||
h('select', {
|
||||
value: selectValue,
|
||||
disabled: loading,
|
||||
onChange: (e) => {
|
||||
const value = e.target.value
|
||||
if (value === '__manual__') {
|
||||
setManual(true)
|
||||
return
|
||||
}
|
||||
setManual(false)
|
||||
if (!value) {
|
||||
setForm({ ...form, imBotId: '', imTargetId: '' })
|
||||
return
|
||||
}
|
||||
const next = parseImOptionKey(value)
|
||||
setForm({ ...form, imBotId: next.botId, imTargetId: next.targetId })
|
||||
},
|
||||
},
|
||||
h('option', { value: '' }, loading ? t('imCatalogLoading') : (
|
||||
options.length === 0 ? t('imTargetNone') : `— ${t('imTarget')} —`
|
||||
)),
|
||||
...options.map((row) => h('option', {
|
||||
key: imOptionKey(row.botId, row.targetId),
|
||||
value: imOptionKey(row.botId, row.targetId),
|
||||
}, `${row.name || row.targetId} · ${row.channel || 'im'} · ${row.targetId}`)),
|
||||
h('option', { value: '__manual__' }, t('imTargetManual')),
|
||||
),
|
||||
),
|
||||
!available || (options.length === 0 && !loading)
|
||||
? h('span', { className: 'dsh-ct-cwdHint' }, imCatalog?.hint || t('imCatalogUnavailable'))
|
||||
: null,
|
||||
showManual
|
||||
? h('div', { className: 'dsh-ct-editorRow' },
|
||||
h('label', null, t('imBotId'), h('input', {
|
||||
value: form.imBotId || '',
|
||||
onChange: (e) => setForm({ ...form, imBotId: e.target.value }),
|
||||
})),
|
||||
h('label', null, t('imTargetId'), h('input', {
|
||||
value: form.imTargetId || '',
|
||||
onChange: (e) => setForm({ ...form, imTargetId: e.target.value }),
|
||||
})),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
|
||||
function JobEditor({ t, form, setForm, isNew, error, lastOutput, nextRunAt, lastRunAt, workspaces, catalog, presets, imCatalog, onSave, onRun, onPause, onRemove }) {
|
||||
return h('div', { className: 'dsh-ct-editor' },
|
||||
h('h1', null, isNew ? t('newJob') : (form.name || t('title'))),
|
||||
h('p', { className: 'dsh-ct-editorLead' }, t('editorLead')),
|
||||
|
|
@ -890,6 +1016,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
),
|
||||
h(CwdField, { t, form, setForm, workspaces }),
|
||||
h(ModelField, { t, form, setForm, catalog }),
|
||||
h(PresetField, { t, form, setForm, presets }),
|
||||
h('label', null, t('delivery'),
|
||||
h('select', {
|
||||
value: form.deliveryKind || 'dsh',
|
||||
|
|
@ -900,16 +1027,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
),
|
||||
),
|
||||
form.deliveryKind === 'im'
|
||||
? h('div', { className: 'dsh-ct-editorRow' },
|
||||
h('label', null, t('imBotId'), h('input', {
|
||||
value: form.imBotId || '',
|
||||
onChange: (e) => setForm({ ...form, imBotId: e.target.value }),
|
||||
})),
|
||||
h('label', null, t('imTargetId'), h('input', {
|
||||
value: form.imTargetId || '',
|
||||
onChange: (e) => setForm({ ...form, imTargetId: e.target.value }),
|
||||
})),
|
||||
)
|
||||
? h(ImDeliveryFields, { t, form, setForm, imCatalog })
|
||||
: null,
|
||||
h('span', { className: 'dsh-ct-cwdHint' }, t('deliveryHint')),
|
||||
h('div', { className: 'dsh-ct-editorActs' },
|
||||
|
|
@ -935,6 +1053,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
const [cronMode, setCronMode] = useState(false)
|
||||
const [workspaces, setWorkspaces] = useState(() => listSnapshot.workspaces)
|
||||
const [catalog, setCatalog] = useState(() => listSnapshot.catalog)
|
||||
const [presets, setPresets] = useState(() => listSnapshot.presets || { items: [], current: null })
|
||||
const [imCatalog, setImCatalog] = useState(() => listSnapshot.imCatalog || { available: true, options: [], loading: false })
|
||||
const [paneHost, setPaneHost] = useState(() => (typeof document === 'undefined' ? null : document.body))
|
||||
const skipAutoSelect = useRef(false)
|
||||
|
||||
|
|
@ -957,18 +1077,44 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
}
|
||||
}, [])
|
||||
|
||||
const loadImCatalog = useCallback(async () => {
|
||||
setImCatalog((prev) => ({ ...prev, loading: true }))
|
||||
try {
|
||||
const body = await api('/im-catalog')
|
||||
const next = {
|
||||
available: body.available !== false,
|
||||
options: Array.isArray(body.options) ? body.options : [],
|
||||
hint: body.hint || '',
|
||||
loading: false,
|
||||
}
|
||||
listSnapshot.imCatalog = next
|
||||
setImCatalog(next)
|
||||
} catch (err) {
|
||||
const next = {
|
||||
available: false,
|
||||
options: [],
|
||||
hint: err instanceof Error ? err.message : String(err),
|
||||
loading: false,
|
||||
}
|
||||
listSnapshot.imCatalog = next
|
||||
setImCatalog(next)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const [jobBody, histBody, wsBody, modelBody] = await Promise.all([
|
||||
const [jobBody, histBody, wsBody, modelBody, presetBody] = await Promise.all([
|
||||
api('/jobs'),
|
||||
api('/history'),
|
||||
api('/workspaces').catch(() => ({ workspaces: [] })),
|
||||
api('/models').catch(() => ({ groups: [], current: null })),
|
||||
api('/presets').catch(() => ({ items: [], current: null })),
|
||||
])
|
||||
const nextJobs = jobBody.jobs || []
|
||||
const nextRuns = histBody.runs || []
|
||||
const nextWorkspaces = wsBody.workspaces || []
|
||||
const nextCatalog = { groups: modelBody.groups || [], current: modelBody.current || null }
|
||||
const nextPresets = { items: presetBody.items || [], current: presetBody.current || null }
|
||||
if (jobStamp(listSnapshot.jobs) !== jobStamp(nextJobs)) {
|
||||
listSnapshot.jobs = nextJobs
|
||||
setJobs(nextJobs)
|
||||
|
|
@ -979,8 +1125,10 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
}
|
||||
listSnapshot.workspaces = nextWorkspaces
|
||||
listSnapshot.catalog = nextCatalog
|
||||
listSnapshot.presets = nextPresets
|
||||
setWorkspaces(nextWorkspaces)
|
||||
setCatalog(nextCatalog)
|
||||
setPresets(nextPresets)
|
||||
faces.cronSessionIds = new Set(nextRuns.map((run) => run.sessionId).filter(Boolean))
|
||||
setError('')
|
||||
} catch (err) {
|
||||
|
|
@ -1002,6 +1150,11 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
}
|
||||
}, [load, cronMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (!cronMode || form.deliveryKind !== 'im') return
|
||||
loadImCatalog()
|
||||
}, [cronMode, form.deliveryKind, loadImCatalog])
|
||||
|
||||
useEffect(() => {
|
||||
if (!cronMode) {
|
||||
skipAutoSelect.current = false
|
||||
|
|
@ -1038,9 +1191,12 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
if ((!next.provider || !next.model) && catalog.current?.provider && catalog.current?.model) {
|
||||
next = { ...next, provider: catalog.current.provider, model: catalog.current.model }
|
||||
}
|
||||
if (!next.agentPreset && presets.current?.id) {
|
||||
next = { ...next, agentPreset: presets.current.id }
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [cronMode, selection.type, catalog.current])
|
||||
}, [cronMode, selection.type, catalog.current, presets.current])
|
||||
|
||||
function selectJob(jobId) {
|
||||
const job = jobs.find((row) => row.id === jobId)
|
||||
|
|
@ -1084,6 +1240,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
name: form.name, prompt: form.prompt, schedule,
|
||||
cwd: form.cwd, timeoutMinutes: form.timeoutMinutes, enabled: form.enabled,
|
||||
provider: form.provider, model: form.model,
|
||||
agentPreset: form.agentPreset || '',
|
||||
delivery: form.deliveryKind === 'im'
|
||||
? { kind: 'im', botId: form.imBotId, targetId: form.imTargetId }
|
||||
: { kind: 'dsh' },
|
||||
|
|
@ -1132,6 +1289,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
lastRunAt: selectedJob?.lastRunAt,
|
||||
workspaces,
|
||||
catalog,
|
||||
presets,
|
||||
imCatalog,
|
||||
onSave: save,
|
||||
onRun: () => selection.jobId && run(selection.jobId),
|
||||
onPause: () => {
|
||||
|
|
@ -1197,7 +1356,10 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
|
||||
function isCronTitle(text) {
|
||||
const value = String(text || '').trim()
|
||||
return value.startsWith('运维定时') || value.startsWith(TITLE_PREFIX.trim()) || value.includes(TITLE_PREFIX)
|
||||
return value.startsWith('定时任务')
|
||||
|| value.startsWith('运维定时')
|
||||
|| value.startsWith(TITLE_PREFIX.trim())
|
||||
|| value.includes(TITLE_PREFIX)
|
||||
}
|
||||
|
||||
function workspaceTitleFromCronFork(title) {
|
||||
|
|
@ -1207,6 +1369,10 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
const stripped = value.replace(/^运维定时(?:\s*·\s*|\s+)/, '').trim()
|
||||
return stripped || value
|
||||
}
|
||||
if (value.startsWith('定时任务')) {
|
||||
const stripped = value.replace(/^定时任务(?:\s*·\s*|\s+)/, '').trim()
|
||||
return stripped || value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
|
|
@ -1219,7 +1385,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
|||
const parent = String(row.parentId || row.parentSessionId || '').trim()
|
||||
if (parent && ids.has(parent)) return true
|
||||
const title = String(row.title || row.displayTitle || '')
|
||||
const looksCron = title.startsWith(TITLE_PREFIX) || title.startsWith('运维定时')
|
||||
const looksCron = title.startsWith(TITLE_PREFIX) || title.startsWith('定时任务') || title.startsWith('运维定时')
|
||||
if (previousId && ids.has(String(previousId)) && looksCron) return true
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue