@mzzsfy/dsh-cron-board 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/package.json +1 -1
- package/src/api.mjs +44 -18
- package/src/client.js +345 -115
- package/src/executor.mjs +10 -1
- package/src/index.js +7 -1
- package/src/schedule-builder.mjs +73 -0
- package/src/scheduler.mjs +3 -39
- package/src/session-driver.mjs +63 -27
- package/test/executor.test.mjs +44 -0
- package/test/index.test.mjs +22 -3
- package/test/parity.test.mjs +1 -1
- package/test/routes.test.mjs +65 -25
- package/test/schedule-builder.test.mjs +123 -0
- package/test/scheduler.test.mjs +0 -131
- package/test/session-driver.test.mjs +167 -31
- package/test/switch-guard.test.mjs +12 -0
- package/src/session-window.mjs +0 -37
package/README.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# @mzzsfy/dsh-cron-board
|
|
2
2
|
|
|
3
|
-
轻量级定时任务看板 dsh 插件:环境变量集中管理 + 定时任务(cron)调度,执行体支持两类——本地脚本(dsh 宿主进程内 spawn)与 dsh 会话任务(
|
|
3
|
+
轻量级定时任务看板 dsh 插件:环境变量集中管理 + 定时任务(cron)调度,执行体支持两类——本地脚本(dsh 宿主进程内 spawn)与 dsh 会话任务(定时向 dsh 会话投递任务,投递即终态,支持「每次新建会话」或「固定同一会话」)。
|
|
4
4
|
|
|
5
|
-
设计文档见仓库 `docs/progress/
|
|
5
|
+
设计文档见仓库 `docs/progress/debug-fix-cron-board-delivery-semantics.md`(近期语义收敛决策记录)。
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- **看板**:任务卡片列表(状态色/下次触发相对时间/启停开关/立即运行/编辑/删除)
|
|
9
|
+
- **看板**:任务卡片列表(状态色/下次触发相对时间/启停开关/立即运行/编辑/删除),新建编辑模态为调度构建器布局:类型/会话模式/调度频率统一分段组件,结构化频率(每天/每周/工作日/固定间隔)自动生成表达式(反解不出即自定义手输),即将执行时刻 chips 为服务端实时预览
|
|
10
10
|
- **环境变量**:集中管理,值打码展示,多值(同名展开笛卡尔积,组合数超出 20 截断并告警),dotenv 文本导入(预览/追加/覆盖)与导出
|
|
11
|
-
-
|
|
12
|
-
- **调度**:croner 解析,停机期间错过不补跑(misfire 即跳过)
|
|
11
|
+
- **会话任务**:投递即终态(crontab 语义,执行状态归会话本身);fresh 每次新建会话;pinned 固定会话(忙时跳过,会话被删除或归档自动重建并回写,首跑自动绑定);按 workdir 自动挂载宿主分组,界面分组内可见;执行预设可选(Agent Preset,缺省跟随宿主默认,dsh-im 同构)
|
|
12
|
+
- **调度**:croner 解析,停机期间错过不补跑(misfire 即跳过),孤儿运行恢复,全局与任务级并发闸门;shell 任务超时收尾(会话任务无超时语义);单次运行开关(runOnce,任意一次真实运行入队后自动停用任务)
|
|
13
13
|
- **日志**:每任务每运行独立日志文件,面板查看与清空,按任务保留条数裁剪(按文件修改时间)
|
|
14
14
|
|
|
15
15
|
## 路由
|
|
16
16
|
|
|
17
|
-
前缀 `/api/cron-board`(envs / jobs / runs / status / cron/preview),REST 风格,中文业务错误透传。
|
|
17
|
+
前缀 `/api/cron-board`(envs / jobs / runs / status / cron/preview / agent-presets),REST 风格,中文业务错误透传。
|
|
18
18
|
|
|
19
19
|
## 界面挂载
|
|
20
20
|
|
|
21
21
|
挂载权交给用户,运行期不自动切换:
|
|
22
22
|
|
|
23
23
|
- **默认:主界面**。无论是否安装 dsh-better-sidebar,看板始终在主界面(原生侧栏「定时任务」入口 + 中心列看板,激活时隐藏会话区,面板互斥)
|
|
24
|
-
- **手动移入**:设置>插件
|
|
24
|
+
- **手动移入**:设置>插件 页「定时任务」卡片(官方 PluginCard 形制:折叠卡壳,cb-pc 镜像)提供「看板移入 better-sidebar 侧边栏」开关;开启即注册扩展槽 tab(显式 openTab 落入工作台)并拆除主界面形态
|
|
25
25
|
- **不自动回退**:移入后页签关闭/类型禁用均不再自动返回主界面;把开关关掉即回到主界面形态(轮询感知,约一个 tick 周期内生效)
|
|
26
26
|
- **开关可用性**:已安装 better-sidebar 时可切换;未安装时开关呈禁用态仅展示(保持主界面,无副作用)
|
|
27
27
|
|
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -19,9 +19,7 @@ export const MESSAGES = {
|
|
|
19
19
|
commandRequired: '任务命令不能为空',
|
|
20
20
|
badKind: '任务类型不合法',
|
|
21
21
|
badSessionMode: '会话模式仅支持 fresh 或 pinned',
|
|
22
|
-
|
|
23
|
-
windowPairRequired: '窗口起止时间必须成对填写',
|
|
24
|
-
badWindowFormat: '窗口时间格式须为 HH:mm',
|
|
22
|
+
badAgentPreset: '执行预设 ID 仅限小写字母、数字与连字符',
|
|
25
23
|
jobNotFound: '任务不存在',
|
|
26
24
|
runNotFound: '运行记录不存在',
|
|
27
25
|
sessionDisabled: '宿主会话服务不可用,会话任务通道已禁用',
|
|
@@ -111,26 +109,22 @@ export function parseEnvText(text) {
|
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
// 任务字段校验与规范化:非法即业务错误;返回可落库字段集
|
|
114
|
-
const WINDOW_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/
|
|
115
112
|
|
|
116
|
-
// 会话任务专有字段(设计 §3.2 job.session 子对象)
|
|
113
|
+
// 会话任务专有字段(设计 §3.2 job.session 子对象);agentPreset 形态对齐宿主预设目录 id(dsh-im 同款校验)
|
|
114
|
+
const AGENT_PRESET_PATTERN = /^[a-z0-9][a-z0-9-]*$/
|
|
115
|
+
|
|
116
|
+
// 会话任务专有字段(设计 §3.2 job.session 子对象)
|
|
117
117
|
function normalizeSession(body) {
|
|
118
118
|
if (body.kind !== 'session') return undefined
|
|
119
119
|
const raw = body.session && typeof body.session === 'object' ? body.session : {}
|
|
120
120
|
if (raw.mode !== undefined && raw.mode !== 'fresh' && raw.mode !== 'pinned') throw new Error(MESSAGES.badSessionMode)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const windowEnd = raw.windowEnd ? String(raw.windowEnd).trim() : ''
|
|
124
|
-
if (windowStart === '' !== (windowEnd === '')) throw new Error(MESSAGES.windowPairRequired)
|
|
125
|
-
for (const window of [windowStart, windowEnd]) {
|
|
126
|
-
if (window !== '' && !WINDOW_PATTERN.test(window)) throw new Error(MESSAGES.badWindowFormat)
|
|
127
|
-
}
|
|
121
|
+
const agentPreset = typeof raw.agentPreset === 'string' ? raw.agentPreset.trim() : ''
|
|
122
|
+
if (agentPreset !== '' && !AGENT_PRESET_PATTERN.test(agentPreset)) throw new Error(MESSAGES.badAgentPreset)
|
|
128
123
|
return {
|
|
129
124
|
mode: raw.mode === 'pinned' ? 'pinned' : 'fresh',
|
|
130
125
|
pinnedSessionId: typeof raw.pinnedSessionId === 'string' ? raw.pinnedSessionId.trim() : '',
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
onMiss: raw.onMiss === 'defer' ? 'defer' : 'skip',
|
|
126
|
+
// 空串 = 跟随宿主当前默认(driver 建会话时对空值省略字段)
|
|
127
|
+
agentPreset,
|
|
134
128
|
}
|
|
135
129
|
}
|
|
136
130
|
|
|
@@ -143,7 +137,6 @@ function normalizeJob(body) {
|
|
|
143
137
|
if (kind === 'shell' && command === '') throw new Error(MESSAGES.commandRequired)
|
|
144
138
|
const schedule = typeof body.schedule === 'string' ? body.schedule.trim() : ''
|
|
145
139
|
assertValidSchedule(schedule)
|
|
146
|
-
const timeoutMs = Number.isInteger(body.timeoutMs) && body.timeoutMs > 0 ? body.timeoutMs : DEFAULT_TIMEOUT_MS
|
|
147
140
|
const normalized = {
|
|
148
141
|
name,
|
|
149
142
|
kind,
|
|
@@ -151,11 +144,17 @@ function normalizeJob(body) {
|
|
|
151
144
|
prompt: typeof body.prompt === 'string' ? body.prompt : '',
|
|
152
145
|
workdir: typeof body.workdir === 'string' ? body.workdir.trim() : '',
|
|
153
146
|
schedule,
|
|
154
|
-
timeoutMs,
|
|
155
147
|
enabled: body.enabled !== false,
|
|
156
148
|
session: normalizeSession(body),
|
|
157
149
|
}
|
|
158
150
|
if (normalized.session === undefined) delete normalized.session
|
|
151
|
+
// 会话任务投递即终态无超时语义,timeoutMs 仅为 shell 真实执行保留
|
|
152
|
+
if (kind === 'shell') {
|
|
153
|
+
normalized.timeoutMs = Number.isInteger(body.timeoutMs) && body.timeoutMs > 0 ? body.timeoutMs : DEFAULT_TIMEOUT_MS
|
|
154
|
+
}
|
|
155
|
+
if (normalized.session === undefined) delete normalized.session
|
|
156
|
+
// 单次运行:真实运行入队即禁用(executor dispatch 承担),缺省关闭
|
|
157
|
+
normalized.runOnce = body.runOnce === true
|
|
159
158
|
// 任务级并发上限:合法正整数才落库,缺省走全局闸门(executor min 规则)
|
|
160
159
|
if (Number.isInteger(body.concurrency) && body.concurrency > 0) normalized.concurrency = body.concurrency
|
|
161
160
|
// nextRunAt:调用方可显式指定(导入任务保留原状态 / 测试注入到期时刻),缺省按 schedule 计算
|
|
@@ -165,7 +164,7 @@ function normalizeJob(body) {
|
|
|
165
164
|
return normalized
|
|
166
165
|
}
|
|
167
166
|
|
|
168
|
-
export function createApi({ store, logger, executor, scheduler, periodic, sessionState, readSidebarTab, updateUiSettings, logSystem }) {
|
|
167
|
+
export function createApi({ store, logger, executor, scheduler, periodic, sessionState, readSidebarTab, updateUiSettings, getAgentPresets, logSystem }) {
|
|
169
168
|
|
|
170
169
|
const routes = [
|
|
171
170
|
{
|
|
@@ -311,6 +310,33 @@ export function createApi({ store, logger, executor, scheduler, periodic, sessio
|
|
|
311
310
|
sendJson(res, 200, { summary: summarizeCron(schedule), nextAt: nextRunsOf(schedule, 3) })
|
|
312
311
|
},
|
|
313
312
|
},
|
|
313
|
+
{
|
|
314
|
+
method: 'GET',
|
|
315
|
+
segments: ['agent-presets'],
|
|
316
|
+
handler: async ({ res }) => {
|
|
317
|
+
// 目录来自宿主 agentPresets 服务(dsh-im 同构);服务缺失或异常回空目录,前端仅展示「跟随宿主默认」
|
|
318
|
+
const catalog = { defaultId: '', items: [] }
|
|
319
|
+
try {
|
|
320
|
+
const service = getAgentPresets ? getAgentPresets() : undefined
|
|
321
|
+
if (service && typeof service.list === 'function') {
|
|
322
|
+
const listed = await service.list()
|
|
323
|
+
const seen = new Set()
|
|
324
|
+
for (const entry of Array.isArray(listed) ? listed : []) {
|
|
325
|
+
const row = typeof entry === 'string' ? { id: entry } : entry
|
|
326
|
+
const id = row && typeof row.id === 'string' ? row.id.trim() : ''
|
|
327
|
+
if (id === '' || !AGENT_PRESET_PATTERN.test(id) || seen.has(id) || row.broken !== undefined) continue
|
|
328
|
+
seen.add(id)
|
|
329
|
+
const label = typeof row.name === 'string' && row.name.trim() ? row.name.trim().slice(0, 128) : id
|
|
330
|
+
catalog.items.push({ id, label })
|
|
331
|
+
}
|
|
332
|
+
if (typeof service.defaultId === 'string' && AGENT_PRESET_PATTERN.test(service.defaultId.trim())) {
|
|
333
|
+
catalog.defaultId = service.defaultId.trim()
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch { /* 目录降级为空,不阻断任务表单 */ }
|
|
337
|
+
sendJson(res, 200, catalog)
|
|
338
|
+
},
|
|
339
|
+
},
|
|
314
340
|
{
|
|
315
341
|
method: 'GET',
|
|
316
342
|
segments: ['jobs'],
|